test: add unit tests for base components (#32818)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-03-02 11:40:43 +08:00
committed by GitHub
parent 8cc775d9f2
commit 335b500aea
401 changed files with 820 additions and 819 deletions

View File

@ -0,0 +1,96 @@
import { fireEvent, render, screen } from '@testing-library/react'
import Alert from '../alert'
describe('Alert', () => {
const defaultProps = {
message: 'This is an alert message',
onHide: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Alert {...defaultProps} />)
expect(screen.getByText(defaultProps.message)).toBeInTheDocument()
})
it('should render the info icon', () => {
render(<Alert {...defaultProps} />)
const icon = screen.getByTestId('info-icon')
expect(icon).toBeInTheDocument()
})
it('should render the close icon', () => {
render(<Alert {...defaultProps} />)
const closeIcon = screen.getByTestId('close-icon')
expect(closeIcon).toBeInTheDocument()
})
})
describe('Props', () => {
it('should apply custom className', () => {
const { container } = render(<Alert {...defaultProps} className="my-custom-class" />)
const outerDiv = container.firstChild as HTMLElement
expect(outerDiv).toHaveClass('my-custom-class')
})
it('should retain base classes when custom className is applied', () => {
const { container } = render(<Alert {...defaultProps} className="my-custom-class" />)
const outerDiv = container.firstChild as HTMLElement
expect(outerDiv).toHaveClass('pointer-events-none', 'w-full')
})
it('should default type to info', () => {
render(<Alert {...defaultProps} />)
const gradientDiv = screen.getByTestId('alert-gradient')
expect(gradientDiv).toHaveClass('from-components-badge-status-light-normal-halo')
})
it('should render with explicit type info', () => {
render(<Alert {...defaultProps} type="info" />)
const gradientDiv = screen.getByTestId('alert-gradient')
expect(gradientDiv).toHaveClass('from-components-badge-status-light-normal-halo')
})
it('should display the provided message text', () => {
const msg = 'A different alert message'
render(<Alert {...defaultProps} message={msg} />)
expect(screen.getByText(msg)).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call onHide when close button is clicked', () => {
const onHide = vi.fn()
render(<Alert {...defaultProps} onHide={onHide} />)
const closeButton = screen.getByTestId('close-icon')
fireEvent.click(closeButton)
expect(onHide).toHaveBeenCalledTimes(1)
})
it('should not call onHide when other parts of the alert are clicked', () => {
const onHide = vi.fn()
render(<Alert {...defaultProps} onHide={onHide} />)
fireEvent.click(screen.getByText(defaultProps.message))
expect(onHide).not.toHaveBeenCalled()
})
})
describe('Edge Cases', () => {
it('should render with an empty message string', () => {
render(<Alert {...defaultProps} message="" />)
const messageDiv = screen.getByTestId('msg-container')
expect(messageDiv).toBeInTheDocument()
expect(messageDiv).toHaveTextContent('')
})
it('should render with a very long message', () => {
const longMessage = 'A'.repeat(1000)
render(<Alert {...defaultProps} message={longMessage} />)
expect(screen.getByText(longMessage)).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,82 @@
import { render, screen } from '@testing-library/react'
import AppUnavailable from '../app-unavailable'
describe('AppUnavailable', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render without crashing', () => {
render(<AppUnavailable />)
expect(screen.getByText(/404/)).toBeInTheDocument()
})
it('should render the error code in a heading', () => {
render(<AppUnavailable />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toHaveTextContent(/404/)
})
it('should render the default unavailable message', () => {
render(<AppUnavailable />)
expect(screen.getByText(/unavailable/i)).toBeInTheDocument()
})
})
describe('Props', () => {
it('should display custom error code', () => {
render(<AppUnavailable code={500} />)
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('500')
})
it('should accept string error code', () => {
render(<AppUnavailable code="403" />)
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('403')
})
it('should apply custom className', () => {
const { container } = render(<AppUnavailable className="my-custom" />)
const outerDiv = container.firstChild as HTMLElement
expect(outerDiv).toHaveClass('my-custom')
})
it('should retain base classes when custom className is applied', () => {
const { container } = render(<AppUnavailable className="my-custom" />)
const outerDiv = container.firstChild as HTMLElement
expect(outerDiv).toHaveClass('flex', 'h-screen', 'w-screen', 'items-center', 'justify-center')
})
it('should display unknownReason when provided', () => {
render(<AppUnavailable unknownReason="Custom error occurred" />)
expect(screen.getByText(/Custom error occurred/i)).toBeInTheDocument()
})
it('should display unknown error translation when isUnknownReason is true', () => {
render(<AppUnavailable isUnknownReason />)
expect(screen.getByText(/share.common.appUnknownError/i)).toBeInTheDocument()
})
it('should prioritize unknownReason over isUnknownReason', () => {
render(<AppUnavailable isUnknownReason unknownReason="My custom reason" />)
expect(screen.getByText(/My custom reason/i)).toBeInTheDocument()
})
it('should show appUnavailable translation when isUnknownReason is false', () => {
render(<AppUnavailable isUnknownReason={false} />)
expect(screen.getByText(/share.common.appUnavailable/i)).toBeInTheDocument()
})
})
describe('Edge Cases', () => {
it('should render with code 0', () => {
render(<AppUnavailable code={0} />)
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('0')
})
it('should render with an empty unknownReason and fall back to translation', () => {
render(<AppUnavailable unknownReason="" />)
expect(screen.getByText(/share.common.appUnavailable/i)).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,86 @@
import { render, screen } from '@testing-library/react'
import Badge from '../badge'
describe('Badge', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Badge text="beta" />)
expect(screen.getByText(/beta/i)).toBeInTheDocument()
})
it('should render with children instead of text', () => {
render(<Badge><span>child content</span></Badge>)
expect(screen.getByText(/child content/i)).toBeInTheDocument()
})
it('should render with no text or children', () => {
const { container } = render(<Badge />)
expect(container.firstChild).toBeInTheDocument()
expect(container.firstChild).toHaveTextContent('')
})
})
describe('Props', () => {
it('should apply custom className', () => {
const { container } = render(<Badge text="test" className="my-custom" />)
const badge = container.firstChild as HTMLElement
expect(badge).toHaveClass('my-custom')
})
it('should retain base classes when custom className is applied', () => {
const { container } = render(<Badge text="test" className="my-custom" />)
const badge = container.firstChild as HTMLElement
expect(badge).toHaveClass('relative', 'inline-flex', 'h-5', 'items-center')
})
it('should apply uppercase class by default', () => {
const { container } = render(<Badge text="test" />)
const badge = container.firstChild as HTMLElement
expect(badge).toHaveClass('system-2xs-medium-uppercase')
})
it('should apply non-uppercase class when uppercase is false', () => {
const { container } = render(<Badge text="test" uppercase={false} />)
const badge = container.firstChild as HTMLElement
expect(badge).toHaveClass('system-xs-medium')
expect(badge).not.toHaveClass('system-2xs-medium-uppercase')
})
it('should render red corner mark when hasRedCornerMark is true', () => {
const { container } = render(<Badge text="test" hasRedCornerMark />)
const mark = container.querySelector('.bg-components-badge-status-light-error-bg')
expect(mark).toBeInTheDocument()
})
it('should not render red corner mark by default', () => {
const { container } = render(<Badge text="test" />)
const mark = container.querySelector('.bg-components-badge-status-light-error-bg')
expect(mark).not.toBeInTheDocument()
})
it('should prioritize children over text', () => {
render(<Badge text="text content"><span>child wins</span></Badge>)
expect(screen.getByText(/child wins/i)).toBeInTheDocument()
expect(screen.queryByText(/text content/i)).not.toBeInTheDocument()
})
it('should render ReactNode as text prop', () => {
render(<Badge text={<strong>bold badge</strong>} />)
expect(screen.getByText(/bold badge/i)).toBeInTheDocument()
})
})
describe('Edge Cases', () => {
it('should render with empty string text', () => {
const { container } = render(<Badge text="" />)
expect(container.firstChild).toBeInTheDocument()
expect(container.firstChild).toHaveTextContent('')
})
it('should render with hasRedCornerMark false explicitly', () => {
const { container } = render(<Badge text="test" hasRedCornerMark={false} />)
const mark = container.querySelector('.bg-components-badge-status-light-error-bg')
expect(mark).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,103 @@
import { fireEvent, render, screen } from '@testing-library/react'
import ThemeSelector from '../theme-selector'
// Mock next-themes with controllable state
let mockTheme = 'system'
const mockSetTheme = vi.fn()
vi.mock('next-themes', () => ({
useTheme: () => ({
theme: mockTheme,
setTheme: mockSetTheme,
}),
}))
describe('ThemeSelector', () => {
beforeEach(() => {
vi.clearAllMocks()
mockTheme = 'system'
})
describe('Rendering', () => {
it('should render without crashing', () => {
const { container } = render(<ThemeSelector />)
expect(container).toBeInTheDocument()
})
it('should render the trigger button', () => {
render(<ThemeSelector />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should not show dropdown content when closed', () => {
render(<ThemeSelector />)
expect(screen.queryByText(/common\.theme\.light/i)).not.toBeInTheDocument()
})
})
describe('Props', () => {
it('should show all theme options when dropdown is opened', () => {
render(<ThemeSelector />)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByText(/light/i)).toBeInTheDocument()
expect(screen.getByText(/dark/i)).toBeInTheDocument()
expect(screen.getByText(/auto/i)).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call setTheme with light when light option is clicked', () => {
render(<ThemeSelector />)
fireEvent.click(screen.getByRole('button'))
const lightButton = screen.getByText(/light/i).closest('button')!
fireEvent.click(lightButton)
expect(mockSetTheme).toHaveBeenCalledWith('light')
})
it('should call setTheme with dark when dark option is clicked', () => {
render(<ThemeSelector />)
fireEvent.click(screen.getByRole('button'))
const darkButton = screen.getByText(/dark/i).closest('button')!
fireEvent.click(darkButton)
expect(mockSetTheme).toHaveBeenCalledWith('dark')
})
it('should call setTheme with system when system option is clicked', () => {
render(<ThemeSelector />)
fireEvent.click(screen.getByRole('button'))
const systemButton = screen.getByText(/auto/i).closest('button')!
fireEvent.click(systemButton)
expect(mockSetTheme).toHaveBeenCalledWith('system')
})
})
describe('Theme-specific rendering', () => {
it('should show checkmark for the currently active light theme', () => {
mockTheme = 'light'
render(<ThemeSelector />)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByTestId('light-icon')).toBeInTheDocument()
})
it('should show checkmark for the currently active dark theme', () => {
mockTheme = 'dark'
render(<ThemeSelector />)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByTestId('dark-icon')).toBeInTheDocument()
})
it('should show checkmark for the currently active system theme', () => {
mockTheme = 'system'
render(<ThemeSelector />)
fireEvent.click(screen.getByRole('button'))
expect(screen.getByTestId('system-icon')).toBeInTheDocument()
})
it('should not show checkmark on non-active themes', () => {
mockTheme = 'light'
render(<ThemeSelector />)
fireEvent.click(screen.getByRole('button'))
expect(screen.queryByTestId('dark-icon')).not.toBeInTheDocument()
expect(screen.queryByTestId('system-icon')).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,106 @@
import { fireEvent, render, screen } from '@testing-library/react'
import ThemeSwitcher from '../theme-switcher'
let mockTheme = 'system'
const mockSetTheme = vi.fn()
vi.mock('next-themes', () => ({
useTheme: () => ({
theme: mockTheme,
setTheme: mockSetTheme,
}),
}))
describe('ThemeSwitcher', () => {
beforeEach(() => {
vi.clearAllMocks()
mockTheme = 'system'
})
describe('Rendering', () => {
it('should render without crashing', () => {
const { container } = render(<ThemeSwitcher />)
expect(container.firstChild).toBeInTheDocument()
})
it('should render three theme option buttons', () => {
render(<ThemeSwitcher />)
expect(screen.getByTestId('system-theme-container')).toBeInTheDocument()
expect(screen.getByTestId('light-theme-container')).toBeInTheDocument()
expect(screen.getByTestId('dark-theme-container')).toBeInTheDocument()
})
it('should render two dividers between options', () => {
render(<ThemeSwitcher />)
const dividers = screen.getAllByTestId('divider')
expect(dividers).toHaveLength(2)
})
})
describe('User Interactions', () => {
it('should call setTheme with system when system option is clicked', () => {
render(<ThemeSwitcher />)
fireEvent.click(screen.getByTestId('system-theme-container')) // system is first
expect(mockSetTheme).toHaveBeenCalledWith('system')
})
it('should call setTheme with light when light option is clicked', () => {
render(<ThemeSwitcher />)
fireEvent.click(screen.getByTestId('light-theme-container')) // light is second
expect(mockSetTheme).toHaveBeenCalledWith('light')
})
it('should call setTheme with dark when dark option is clicked', () => {
render(<ThemeSwitcher />)
fireEvent.click(screen.getByTestId('dark-theme-container')) // dark is third
expect(mockSetTheme).toHaveBeenCalledWith('dark')
})
})
describe('Theme-specific rendering', () => {
it('should highlight system option when theme is system', () => {
mockTheme = 'system'
render(<ThemeSwitcher />)
expect(screen.getByTestId('system-theme-container')).toHaveClass('bg-components-segmented-control-item-active-bg')
expect(screen.getByTestId('light-theme-container')).not.toHaveClass('bg-components-segmented-control-item-active-bg')
expect(screen.getByTestId('dark-theme-container')).not.toHaveClass('bg-components-segmented-control-item-active-bg')
})
it('should highlight light option when theme is light', () => {
mockTheme = 'light'
render(<ThemeSwitcher />)
expect(screen.getByTestId('light-theme-container')).toHaveClass('bg-components-segmented-control-item-active-bg')
expect(screen.getByTestId('system-theme-container')).not.toHaveClass('bg-components-segmented-control-item-active-bg')
expect(screen.getByTestId('dark-theme-container')).not.toHaveClass('bg-components-segmented-control-item-active-bg')
})
it('should highlight dark option when theme is dark', () => {
mockTheme = 'dark'
render(<ThemeSwitcher />)
expect(screen.getByTestId('dark-theme-container')).toHaveClass('bg-components-segmented-control-item-active-bg')
expect(screen.getByTestId('system-theme-container')).not.toHaveClass('bg-components-segmented-control-item-active-bg')
expect(screen.getByTestId('light-theme-container')).not.toHaveClass('bg-components-segmented-control-item-active-bg')
})
it('should show divider between system and light when dark is active', () => {
mockTheme = 'dark'
render(<ThemeSwitcher />)
const dividers = screen.getAllByTestId('divider')
expect(dividers[0]).toHaveClass('bg-divider-regular')
})
it('should show divider between light and dark when system is active', () => {
mockTheme = 'system'
render(<ThemeSwitcher />)
const dividers = screen.getAllByTestId('divider')
expect(dividers[1]).toHaveClass('bg-divider-regular')
})
it('should have transparent dividers when neither adjacent theme is active', () => {
mockTheme = 'light'
render(<ThemeSwitcher />)
const dividers = screen.getAllByTestId('divider')
expect(dividers[0]).not.toHaveClass('bg-divider-regular')
expect(dividers[1]).not.toHaveClass('bg-divider-regular')
})
})
})