diff --git a/web/app/components/base/app-icon/index.spec.tsx b/web/app/components/base/app-icon/index.spec.tsx
new file mode 100644
index 0000000000..b6d87ba7d8
--- /dev/null
+++ b/web/app/components/base/app-icon/index.spec.tsx
@@ -0,0 +1,159 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import '@testing-library/jest-dom'
+import AppIcon from './index'
+
+// Mock emoji-mart initialization
+jest.mock('emoji-mart', () => ({
+ init: jest.fn(),
+}))
+
+// Mock emoji data
+jest.mock('@emoji-mart/data', () => ({}))
+
+// Mock the ahooks useHover hook
+jest.mock('ahooks', () => ({
+ useHover: jest.fn(() => false),
+}))
+
+describe('AppIcon', () => {
+ beforeEach(() => {
+ // Mock custom element
+ if (!customElements.get('em-emoji')) {
+ customElements.define('em-emoji', class extends HTMLElement {
+ constructor() {
+ super()
+ }
+
+ // Mock basic functionality
+ connectedCallback() {
+ this.innerHTML = '🤖'
+ }
+ })
+ }
+
+ // Reset mocks
+ require('ahooks').useHover.mockReset().mockReturnValue(false)
+ })
+
+ it('renders default emoji when no icon or image is provided', () => {
+ render()
+ const emojiElement = document.querySelector('em-emoji')
+ expect(emojiElement).toBeInTheDocument()
+ expect(emojiElement?.getAttribute('id')).toBe('🤖')
+ })
+
+ it('renders with custom emoji when icon is provided', () => {
+ render()
+ const emojiElement = document.querySelector('em-emoji')
+ expect(emojiElement).toBeInTheDocument()
+ expect(emojiElement?.getAttribute('id')).toBe('smile')
+ })
+
+ it('renders image when iconType is image and imageUrl is provided', () => {
+ render()
+ const imgElement = screen.getByAltText('app icon')
+ expect(imgElement).toBeInTheDocument()
+ expect(imgElement).toHaveAttribute('src', 'test-image.jpg')
+ })
+
+ it('renders innerIcon when provided', () => {
+ render(Custom Icon} />)
+ const innerIcon = screen.getByTestId('inner-icon')
+ expect(innerIcon).toBeInTheDocument()
+ })
+
+ it('applies size classes correctly', () => {
+ const { container: xsContainer } = render()
+ expect(xsContainer.firstChild).toHaveClass('w-4 h-4 rounded-[4px]')
+
+ const { container: tinyContainer } = render()
+ expect(tinyContainer.firstChild).toHaveClass('w-6 h-6 rounded-md')
+
+ const { container: smallContainer } = render()
+ expect(smallContainer.firstChild).toHaveClass('w-8 h-8 rounded-lg')
+
+ const { container: mediumContainer } = render()
+ expect(mediumContainer.firstChild).toHaveClass('w-9 h-9 rounded-[10px]')
+
+ const { container: largeContainer } = render()
+ expect(largeContainer.firstChild).toHaveClass('w-10 h-10 rounded-[10px]')
+
+ const { container: xlContainer } = render()
+ expect(xlContainer.firstChild).toHaveClass('w-12 h-12 rounded-xl')
+
+ const { container: xxlContainer } = render()
+ expect(xxlContainer.firstChild).toHaveClass('w-14 h-14 rounded-2xl')
+ })
+
+ it('applies rounded class when rounded=true', () => {
+ const { container } = render()
+ expect(container.firstChild).toHaveClass('rounded-full')
+ })
+
+ it('applies custom background color', () => {
+ const { container } = render()
+ expect(container.firstChild).toHaveStyle('background: #FF5500')
+ })
+
+ it('uses default background color when no background is provided for non-image icons', () => {
+ const { container } = render()
+ expect(container.firstChild).toHaveStyle('background: #FFEAD5')
+ })
+
+ it('does not apply background style for image icons', () => {
+ const { container } = render()
+ // Should not have the background style from the prop
+ expect(container.firstChild).not.toHaveStyle('background: #FF5500')
+ })
+
+ it('calls onClick handler when clicked', () => {
+ const handleClick = jest.fn()
+ const { container } = render()
+ fireEvent.click(container.firstChild!)
+
+ expect(handleClick).toHaveBeenCalledTimes(1)
+ })
+
+ it('applies custom className', () => {
+ const { container } = render()
+ expect(container.firstChild).toHaveClass('custom-class')
+ })
+
+ it('does not display edit icon when showEditIcon=false', () => {
+ render()
+ const editIcon = screen.queryByRole('svg')
+ expect(editIcon).not.toBeInTheDocument()
+ })
+
+ it('displays edit icon when showEditIcon=true and hovering', () => {
+ // Mock the useHover hook to return true for this test
+ require('ahooks').useHover.mockReturnValue(true)
+
+ render()
+ const editIcon = document.querySelector('svg')
+ expect(editIcon).toBeInTheDocument()
+ })
+
+ it('does not display edit icon when showEditIcon=true but not hovering', () => {
+ // useHover returns false by default from our mock setup
+ render()
+ const editIcon = document.querySelector('svg')
+ expect(editIcon).not.toBeInTheDocument()
+ })
+
+ it('handles conditional isValidImageIcon check correctly', () => {
+ // Case 1: Valid image icon
+ const { rerender } = render(
+ ,
+ )
+ expect(screen.getByAltText('app icon')).toBeInTheDocument()
+
+ // Case 2: Invalid - missing image URL
+ rerender()
+ expect(screen.queryByAltText('app icon')).not.toBeInTheDocument()
+
+ // Case 3: Invalid - wrong icon type
+ rerender()
+ expect(screen.queryByAltText('app icon')).not.toBeInTheDocument()
+ })
+})