test(web): add comprehensive unit and integration tests for plugins and tools modules (#32220)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-02-12 10:04:56 +08:00
committed by GitHub
parent 10f85074e8
commit d6b025e91e
195 changed files with 12219 additions and 7840 deletions

View File

@ -0,0 +1,259 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Theme } from '@/types/app'
import IconWithTooltip from '../icon-with-tooltip'
// Mock Tooltip component
vi.mock('@/app/components/base/tooltip', () => ({
default: ({
children,
popupContent,
popupClassName,
}: {
children: React.ReactNode
popupContent?: string
popupClassName?: string
}) => (
<div data-testid="tooltip" data-popup-content={popupContent} data-popup-classname={popupClassName}>
{children}
</div>
),
}))
// Mock icon components
const MockLightIcon = ({ className }: { className?: string }) => (
<div data-testid="light-icon" className={className}>Light Icon</div>
)
const MockDarkIcon = ({ className }: { className?: string }) => (
<div data-testid="dark-icon" className={className}>Dark Icon</div>
)
describe('IconWithTooltip', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render without crashing', () => {
render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
})
it('should render Tooltip wrapper', () => {
render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
popupContent="Test tooltip"
/>,
)
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', 'Test tooltip')
})
it('should apply correct popupClassName to Tooltip', () => {
render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
const tooltip = screen.getByTestId('tooltip')
expect(tooltip).toHaveAttribute('data-popup-classname')
expect(tooltip.getAttribute('data-popup-classname')).toContain('border-components-panel-border')
})
})
describe('Theme Handling', () => {
it('should render light icon when theme is light', () => {
render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
expect(screen.getByTestId('light-icon')).toBeInTheDocument()
expect(screen.queryByTestId('dark-icon')).not.toBeInTheDocument()
})
it('should render dark icon when theme is dark', () => {
render(
<IconWithTooltip
theme={Theme.dark}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
expect(screen.getByTestId('dark-icon')).toBeInTheDocument()
expect(screen.queryByTestId('light-icon')).not.toBeInTheDocument()
})
it('should render light icon when theme is system (not dark)', () => {
render(
<IconWithTooltip
theme={'system' as Theme}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
// When theme is not 'dark', it should use light icon
expect(screen.getByTestId('light-icon')).toBeInTheDocument()
})
})
describe('Props', () => {
it('should apply custom className to icon', () => {
render(
<IconWithTooltip
className="custom-class"
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
const icon = screen.getByTestId('light-icon')
expect(icon).toHaveClass('custom-class')
})
it('should apply default h-5 w-5 class to icon', () => {
render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
const icon = screen.getByTestId('light-icon')
expect(icon).toHaveClass('h-5')
expect(icon).toHaveClass('w-5')
})
it('should merge custom className with default classes', () => {
render(
<IconWithTooltip
className="ml-2"
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
const icon = screen.getByTestId('light-icon')
expect(icon).toHaveClass('h-5')
expect(icon).toHaveClass('w-5')
expect(icon).toHaveClass('ml-2')
})
it('should pass popupContent to Tooltip', () => {
render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
popupContent="Custom tooltip content"
/>,
)
expect(screen.getByTestId('tooltip')).toHaveAttribute(
'data-popup-content',
'Custom tooltip content',
)
})
it('should handle undefined popupContent', () => {
render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
})
})
describe('Memoization', () => {
it('should be wrapped with React.memo', () => {
// The component is exported as React.memo(IconWithTooltip)
expect(IconWithTooltip).toBeDefined()
// Check if it's a memo component
expect(typeof IconWithTooltip).toBe('object')
})
})
describe('Container Structure', () => {
it('should render icon inside flex container', () => {
const { container } = render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
const flexContainer = container.querySelector('.flex.shrink-0.items-center.justify-center')
expect(flexContainer).toBeInTheDocument()
})
})
describe('Edge Cases', () => {
it('should handle empty className', () => {
render(
<IconWithTooltip
className=""
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
/>,
)
expect(screen.getByTestId('light-icon')).toBeInTheDocument()
})
it('should handle long popupContent', () => {
const longContent = 'A'.repeat(500)
render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
popupContent={longContent}
/>,
)
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', longContent)
})
it('should handle special characters in popupContent', () => {
const specialContent = '<script>alert("xss")</script> & "quotes"'
render(
<IconWithTooltip
theme={Theme.light}
BadgeIconLight={MockLightIcon}
BadgeIconDark={MockDarkIcon}
popupContent={specialContent}
/>,
)
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', specialContent)
})
})
})

View File

@ -0,0 +1,205 @@
import type { ComponentProps } from 'react'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Theme } from '@/types/app'
import Partner from '../partner'
// Mock useTheme hook
const mockUseTheme = vi.fn()
vi.mock('@/hooks/use-theme', () => ({
default: () => mockUseTheme(),
}))
// Mock IconWithTooltip to directly test Partner's behavior
type IconWithTooltipProps = ComponentProps<typeof import('../icon-with-tooltip').default>
const mockIconWithTooltip = vi.fn()
vi.mock('../icon-with-tooltip', () => ({
default: (props: IconWithTooltipProps) => {
mockIconWithTooltip(props)
const { theme, BadgeIconLight, BadgeIconDark, className, popupContent } = props
const isDark = theme === Theme.dark
const Icon = isDark ? BadgeIconDark : BadgeIconLight
return (
<div data-testid="icon-with-tooltip" data-popup-content={popupContent} data-theme={theme}>
<Icon className={className} data-testid={isDark ? 'partner-dark-icon' : 'partner-light-icon'} />
</div>
)
},
}))
// Mock Partner icons
vi.mock('@/app/components/base/icons/src/public/plugins/PartnerDark', () => ({
default: ({ className, ...rest }: { className?: string }) => (
<div data-testid="partner-dark-icon" className={className} {...rest}>PartnerDark</div>
),
}))
vi.mock('@/app/components/base/icons/src/public/plugins/PartnerLight', () => ({
default: ({ className, ...rest }: { className?: string }) => (
<div data-testid="partner-light-icon" className={className} {...rest}>PartnerLight</div>
),
}))
describe('Partner', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseTheme.mockReturnValue({ theme: Theme.light })
mockIconWithTooltip.mockClear()
})
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Partner text="Partner Tip" />)
expect(screen.getByTestId('icon-with-tooltip')).toBeInTheDocument()
})
it('should call useTheme hook', () => {
render(<Partner text="Partner" />)
expect(mockUseTheme).toHaveBeenCalled()
})
it('should pass text prop as popupContent to IconWithTooltip', () => {
render(<Partner text="This is a partner" />)
expect(screen.getByTestId('icon-with-tooltip')).toHaveAttribute(
'data-popup-content',
'This is a partner',
)
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({ popupContent: 'This is a partner' }),
)
})
it('should pass theme from useTheme to IconWithTooltip', () => {
mockUseTheme.mockReturnValue({ theme: Theme.light })
render(<Partner text="Partner" />)
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({ theme: Theme.light }),
)
})
it('should render light icon in light theme', () => {
mockUseTheme.mockReturnValue({ theme: Theme.light })
render(<Partner text="Partner" />)
expect(screen.getByTestId('partner-light-icon')).toBeInTheDocument()
})
it('should render dark icon in dark theme', () => {
mockUseTheme.mockReturnValue({ theme: Theme.dark })
render(<Partner text="Partner" />)
expect(screen.getByTestId('partner-dark-icon')).toBeInTheDocument()
})
})
describe('Props', () => {
it('should pass className to IconWithTooltip', () => {
render(<Partner className="custom-class" text="Partner" />)
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({ className: 'custom-class' }),
)
})
it('should pass correct BadgeIcon components to IconWithTooltip', () => {
render(<Partner text="Partner" />)
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({
BadgeIconLight: expect.any(Function),
BadgeIconDark: expect.any(Function),
}),
)
})
})
describe('Theme Handling', () => {
it('should handle light theme correctly', () => {
mockUseTheme.mockReturnValue({ theme: Theme.light })
render(<Partner text="Partner" />)
expect(mockUseTheme).toHaveBeenCalled()
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({ theme: Theme.light }),
)
expect(screen.getByTestId('partner-light-icon')).toBeInTheDocument()
})
it('should handle dark theme correctly', () => {
mockUseTheme.mockReturnValue({ theme: Theme.dark })
render(<Partner text="Partner" />)
expect(mockUseTheme).toHaveBeenCalled()
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({ theme: Theme.dark }),
)
expect(screen.getByTestId('partner-dark-icon')).toBeInTheDocument()
})
it('should pass updated theme when theme changes', () => {
mockUseTheme.mockReturnValue({ theme: Theme.light })
const { rerender } = render(<Partner text="Partner" />)
expect(mockIconWithTooltip).toHaveBeenLastCalledWith(
expect.objectContaining({ theme: Theme.light }),
)
mockIconWithTooltip.mockClear()
mockUseTheme.mockReturnValue({ theme: Theme.dark })
rerender(<Partner text="Partner" />)
expect(mockIconWithTooltip).toHaveBeenLastCalledWith(
expect.objectContaining({ theme: Theme.dark }),
)
})
})
describe('Edge Cases', () => {
it('should handle empty text', () => {
render(<Partner text="" />)
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({ popupContent: '' }),
)
})
it('should handle long text', () => {
const longText = 'A'.repeat(500)
render(<Partner text={longText} />)
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({ popupContent: longText }),
)
})
it('should handle special characters in text', () => {
const specialText = '<script>alert("xss")</script>'
render(<Partner text={specialText} />)
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({ popupContent: specialText }),
)
})
it('should handle undefined className', () => {
render(<Partner text="Partner" />)
expect(mockIconWithTooltip).toHaveBeenCalledWith(
expect.objectContaining({ className: undefined }),
)
})
it('should always call useTheme to get current theme', () => {
render(<Partner text="Partner 1" />)
expect(mockUseTheme).toHaveBeenCalledTimes(1)
mockUseTheme.mockClear()
render(<Partner text="Partner 2" />)
expect(mockUseTheme).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -0,0 +1,52 @@
import { render, screen } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/app/components/base/icons/src/public/plugins/VerifiedDark', () => ({
default: () => <span data-testid="verified-dark" />,
}))
vi.mock('@/app/components/base/icons/src/public/plugins/VerifiedLight', () => ({
default: () => <span data-testid="verified-light" />,
}))
vi.mock('@/hooks/use-theme', () => ({
default: () => ({ theme: 'light' }),
}))
vi.mock('../icon-with-tooltip', () => ({
default: ({ popupContent, BadgeIconLight, BadgeIconDark, theme }: {
popupContent: string
BadgeIconLight: React.FC
BadgeIconDark: React.FC
theme: string
[key: string]: unknown
}) => (
<div data-testid="icon-with-tooltip" data-popup={popupContent}>
{theme === 'light' ? <BadgeIconLight /> : <BadgeIconDark />}
</div>
),
}))
describe('Verified', () => {
let Verified: (typeof import('../verified'))['default']
beforeEach(async () => {
vi.clearAllMocks()
const mod = await import('../verified')
Verified = mod.default
})
it('should render with tooltip text', () => {
render(<Verified text="Verified Plugin" />)
const tooltip = screen.getByTestId('icon-with-tooltip')
expect(tooltip).toHaveAttribute('data-popup', 'Verified Plugin')
})
it('should render light theme icon by default', () => {
render(<Verified text="Verified" />)
expect(screen.getByTestId('verified-light')).toBeInTheDocument()
})
})