mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
test: add unit tests for base components (#32818)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
96
web/app/components/base/__tests__/alert.spec.tsx
Normal file
96
web/app/components/base/__tests__/alert.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
82
web/app/components/base/__tests__/app-unavailable.spec.tsx
Normal file
82
web/app/components/base/__tests__/app-unavailable.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
86
web/app/components/base/__tests__/badge.spec.tsx
Normal file
86
web/app/components/base/__tests__/badge.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
103
web/app/components/base/__tests__/theme-selector.spec.tsx
Normal file
103
web/app/components/base/__tests__/theme-selector.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
106
web/app/components/base/__tests__/theme-switcher.spec.tsx
Normal file
106
web/app/components/base/__tests__/theme-switcher.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user