chore: add unit test to high frequency component (#17423)

This commit is contained in:
Joel 2025-04-03 18:19:11 +08:00 committed by GitHub
parent 2e9997110a
commit 31a6aabfe5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 924 additions and 41 deletions

View File

@ -4,46 +4,107 @@ import Button from './index'
afterEach(cleanup)
// https://testing-library.com/docs/queries/about
describe('Button text', () => {
test('Button text should be same as children', async () => {
const { getByRole, container } = render(<Button>Click me</Button>)
expect(getByRole('button').textContent).toBe('Click me')
expect(container.querySelector('button')?.textContent).toBe('Click me')
describe('Button', () => {
describe('Button text', () => {
test('Button text should be same as children', async () => {
const { getByRole, container } = render(<Button>Click me</Button>)
expect(getByRole('button').textContent).toBe('Click me')
expect(container.querySelector('button')?.textContent).toBe('Click me')
})
})
test('Loading button text should include same as children', async () => {
const { getByRole } = render(<Button loading>Click me</Button>)
expect(getByRole('button').textContent?.includes('Loading')).toBe(true)
})
})
describe('Button style', () => {
test('Button should have default variant', async () => {
const { getByRole } = render(<Button>Click me</Button>)
expect(getByRole('button').className).toContain('btn-secondary')
})
test('Button should have primary variant', async () => {
const { getByRole } = render(<Button variant='primary'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-primary')
})
test('Button should have warning variant', async () => {
const { getByRole } = render(<Button variant='warning'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-warning')
})
test('Button disabled should have disabled variant', async () => {
const { getByRole } = render(<Button disabled>Click me</Button>)
expect(getByRole('button').className).toContain('btn-disabled')
})
})
describe('Button events', () => {
test('onClick should been call after clicked', async () => {
const onClick = jest.fn()
const { getByRole } = render(<Button onClick={onClick}>Click me</Button>)
fireEvent.click(getByRole('button'))
expect(onClick).toHaveBeenCalled()
describe('Button loading', () => {
test('Loading button text should include same as children', async () => {
const { getByRole } = render(<Button loading>Click me</Button>)
expect(getByRole('button').textContent?.includes('Loading')).toBe(true)
})
test('Not loading button text should include same as children', async () => {
const { getByRole } = render(<Button loading={false}>Click me</Button>)
expect(getByRole('button').textContent?.includes('Loading')).toBe(false)
})
test('Loading button should have loading classname', async () => {
const animClassName = 'anim-breath'
const { getByRole } = render(<Button loading spinnerClassName={animClassName}>Click me</Button>)
expect(getByRole('button').getElementsByClassName('animate-spin')[0]?.className).toContain(animClassName)
})
})
describe('Button style', () => {
test('Button should have default variant', async () => {
const { getByRole } = render(<Button>Click me</Button>)
expect(getByRole('button').className).toContain('btn-secondary')
})
test('Button should have primary variant', async () => {
const { getByRole } = render(<Button variant='primary'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-primary')
})
test('Button should have warning variant', async () => {
const { getByRole } = render(<Button variant='warning'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-warning')
})
test('Button should have secondary variant', async () => {
const { getByRole } = render(<Button variant='secondary'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-secondary')
})
test('Button should have secondary-accent variant', async () => {
const { getByRole } = render(<Button variant='secondary-accent'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-secondary-accent')
})
test('Button should have ghost variant', async () => {
const { getByRole } = render(<Button variant='ghost'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-ghost')
})
test('Button should have ghost-accent variant', async () => {
const { getByRole } = render(<Button variant='ghost-accent'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-ghost-accent')
})
test('Button disabled should have disabled variant', async () => {
const { getByRole } = render(<Button disabled>Click me</Button>)
expect(getByRole('button').className).toContain('btn-disabled')
})
})
describe('Button size', () => {
test('Button should have default size', async () => {
const { getByRole } = render(<Button>Click me</Button>)
expect(getByRole('button').className).toContain('btn-medium')
})
test('Button should have small size', async () => {
const { getByRole } = render(<Button size='small'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-small')
})
test('Button should have medium size', async () => {
const { getByRole } = render(<Button size='medium'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-medium')
})
test('Button should have large size', async () => {
const { getByRole } = render(<Button size='large'>Click me</Button>)
expect(getByRole('button').className).toContain('btn-large')
})
})
describe('Button destructive', () => {
test('Button should have destructive classname', async () => {
const { getByRole } = render(<Button destructive>Click me</Button>)
expect(getByRole('button').className).toContain('btn-destructive')
})
})
describe('Button events', () => {
test('onClick should been call after clicked', async () => {
const onClick = jest.fn()
const { getByRole } = render(<Button onClick={onClick}>Click me</Button>)
fireEvent.click(getByRole('button'))
expect(onClick).toHaveBeenCalled()
})
})
})

View File

@ -0,0 +1,55 @@
import { render } from '@testing-library/react'
import '@testing-library/jest-dom'
import Divider from './index'
describe('Divider', () => {
it('renders with default props', () => {
const { container } = render(<Divider />)
const divider = container.firstChild as HTMLElement
expect(divider).toHaveClass('w-full h-[0.5px] my-2')
expect(divider).toHaveClass('bg-divider-regular')
})
it('renders horizontal solid divider correctly', () => {
const { container } = render(<Divider type="horizontal" bgStyle="solid" />)
const divider = container.firstChild as HTMLElement
expect(divider).toHaveClass('w-full h-[0.5px] my-2')
expect(divider).toHaveClass('bg-divider-regular')
})
it('renders vertical solid divider correctly', () => {
const { container } = render(<Divider type="vertical" bgStyle="solid" />)
const divider = container.firstChild as HTMLElement
expect(divider).toHaveClass('w-[1px] h-full mx-2')
expect(divider).toHaveClass('bg-divider-regular')
})
it('renders horizontal gradient divider correctly', () => {
const { container } = render(<Divider type="horizontal" bgStyle="gradient" />)
const divider = container.firstChild as HTMLElement
expect(divider).toHaveClass('w-full h-[0.5px] my-2')
expect(divider).toHaveClass('bg-gradient-to-r from-divider-regular to-background-gradient-mask-transparent')
})
it('renders vertical gradient divider correctly', () => {
const { container } = render(<Divider type="vertical" bgStyle="gradient" />)
const divider = container.firstChild as HTMLElement
expect(divider).toHaveClass('w-[1px] h-full mx-2')
expect(divider).toHaveClass('bg-gradient-to-r from-divider-regular to-background-gradient-mask-transparent')
})
it('applies custom className correctly', () => {
const customClass = 'test-custom-class'
const { container } = render(<Divider className={customClass} />)
const divider = container.firstChild as HTMLElement
expect(divider).toHaveClass(customClass)
expect(divider).toHaveClass('w-full h-[0.5px] my-2')
})
it('applies custom style correctly', () => {
const customStyle = { margin: '10px' }
const { container } = render(<Divider style={customStyle} />)
const divider = container.firstChild as HTMLElement
expect(divider).toHaveStyle('margin: 10px')
})
})

View File

@ -0,0 +1,67 @@
import { fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import React from 'react'
import type { IconData } from './IconBase'
import IconBase from './IconBase'
import * as utils from './utils'
// Mock the utils module
jest.mock('./utils', () => ({
generate: jest.fn((icon, key, props) => (
<svg
data-testid="mock-svg"
key={key}
{...props}
>
mocked svg content
</svg>
)),
}))
describe('IconBase Component', () => {
const mockData: IconData = {
name: 'test-icon',
icon: { name: 'svg', attributes: {}, children: [] },
}
beforeEach(() => {
jest.clearAllMocks()
})
it('renders properly with required props', () => {
render(<IconBase data={mockData} />)
const svg = screen.getByTestId('mock-svg')
expect(svg).toBeInTheDocument()
expect(svg).toHaveAttribute('data-icon', mockData.name)
expect(svg).toHaveAttribute('aria-hidden', 'true')
})
it('passes className to the generated SVG', () => {
render(<IconBase data={mockData} className="custom-class" />)
const svg = screen.getByTestId('mock-svg')
expect(svg).toHaveAttribute('class', 'custom-class')
expect(utils.generate).toHaveBeenCalledWith(
mockData.icon,
'svg-test-icon',
expect.objectContaining({ className: 'custom-class' }),
)
})
it('handles onClick events', () => {
const handleClick = jest.fn()
render(<IconBase data={mockData} onClick={handleClick} />)
const svg = screen.getByTestId('mock-svg')
fireEvent.click(svg)
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('applies custom styles', () => {
const customStyle = { color: 'red', fontSize: '24px' }
render(<IconBase data={mockData} style={customStyle} />)
expect(utils.generate).toHaveBeenCalledWith(
mockData.icon,
'svg-test-icon',
expect.objectContaining({ style: customStyle }),
)
})
})

View File

@ -0,0 +1,70 @@
import type { AbstractNode } from './utils'
import { generate, normalizeAttrs } from './utils'
import { render } from '@testing-library/react'
import '@testing-library/jest-dom'
describe('generate icon base utils', () => {
describe('normalizeAttrs', () => {
it('should normalize class to className', () => {
const attrs = { class: 'test-class' }
const result = normalizeAttrs(attrs)
expect(result).toEqual({ className: 'test-class' })
})
it('should normalize style string to style object', () => {
const attrs = { style: 'color:red;font-size:14px;' }
const result = normalizeAttrs(attrs)
expect(result).toEqual({ style: { color: 'red', fontSize: '14px' } })
})
it('should handle attributes with dashes and colons', () => {
const attrs = { 'data-test': 'value', 'xlink:href': 'url' }
const result = normalizeAttrs(attrs)
expect(result).toEqual({ dataTest: 'value', xlinkHref: 'url' })
})
})
describe('generate', () => {
it('should generate React elements from AbstractNode', () => {
const node: AbstractNode = {
name: 'div',
attributes: { class: 'container' },
children: [
{
name: 'span',
attributes: { style: 'color:blue;' },
children: [],
},
],
}
const { container } = render(generate(node, 'key'))
// to svg element
expect(container.firstChild).toHaveClass('container')
expect(container.querySelector('span')).toHaveStyle({ color: 'blue' })
})
// add not has children
it('should generate React elements without children', () => {
const node: AbstractNode = {
name: 'div',
attributes: { class: 'container' },
}
const { container } = render(generate(node, 'key'))
// to svg element
expect(container.firstChild).toHaveClass('container')
})
it('should merge rootProps when provided', () => {
const node: AbstractNode = {
name: 'div',
attributes: { class: 'container' },
children: [],
}
const rootProps = { id: 'root' }
const { container } = render(generate(node, 'key', rootProps))
expect(container.querySelector('div')).toHaveAttribute('id', 'root')
})
})
})

View File

@ -0,0 +1,124 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import Input, { inputVariants } from './index'
// Mock the i18n hook
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'common.operation.search': 'Search',
'common.placeholder.input': 'Please input',
}
return translations[key] || ''
},
}),
}))
describe('Input component', () => {
describe('Variants', () => {
it('should return correct classes for regular size', () => {
const result = inputVariants({ size: 'regular' })
expect(result).toContain('px-3')
expect(result).toContain('radius-md')
expect(result).toContain('system-sm-regular')
})
it('should return correct classes for large size', () => {
const result = inputVariants({ size: 'large' })
expect(result).toContain('px-4')
expect(result).toContain('radius-lg')
expect(result).toContain('system-md-regular')
})
it('should use regular size as default', () => {
const result = inputVariants({})
expect(result).toContain('px-3')
expect(result).toContain('radius-md')
expect(result).toContain('system-sm-regular')
})
})
it('renders correctly with default props', () => {
render(<Input />)
const input = screen.getByPlaceholderText('Please input')
expect(input).toBeInTheDocument()
expect(input).not.toBeDisabled()
expect(input).not.toHaveClass('cursor-not-allowed')
})
it('shows left icon when showLeftIcon is true', () => {
render(<Input showLeftIcon />)
const searchIcon = document.querySelector('svg')
expect(searchIcon).toBeInTheDocument()
const input = screen.getByPlaceholderText('Search')
expect(input).toHaveClass('pl-[26px]')
})
it('shows clear icon when showClearIcon is true and has value', () => {
render(<Input showClearIcon value="test" />)
const clearIcon = document.querySelector('.group svg')
expect(clearIcon).toBeInTheDocument()
const input = screen.getByDisplayValue('test')
expect(input).toHaveClass('pr-[26px]')
})
it('does not show clear icon when disabled, even with value', () => {
render(<Input showClearIcon value="test" disabled />)
const clearIcon = document.querySelector('.group svg')
expect(clearIcon).not.toBeInTheDocument()
})
it('calls onClear when clear icon is clicked', () => {
const onClear = jest.fn()
render(<Input showClearIcon value="test" onClear={onClear} />)
const clearIconContainer = document.querySelector('.group')
fireEvent.click(clearIconContainer!)
expect(onClear).toHaveBeenCalledTimes(1)
})
it('shows warning icon when destructive is true', () => {
render(<Input destructive />)
const warningIcon = document.querySelector('svg')
expect(warningIcon).toBeInTheDocument()
const input = screen.getByPlaceholderText('Please input')
expect(input).toHaveClass('border-components-input-border-destructive')
})
it('applies disabled styles when disabled', () => {
render(<Input disabled />)
const input = screen.getByPlaceholderText('Please input')
expect(input).toBeDisabled()
expect(input).toHaveClass('cursor-not-allowed')
expect(input).toHaveClass('bg-components-input-bg-disabled')
})
it('displays custom unit when provided', () => {
render(<Input unit="km" />)
const unitElement = screen.getByText('km')
expect(unitElement).toBeInTheDocument()
})
it('applies custom className and style', () => {
const customClass = 'test-class'
const customStyle = { color: 'red' }
render(<Input className={customClass} styleCss={customStyle} />)
const input = screen.getByPlaceholderText('Please input')
expect(input).toHaveClass(customClass)
expect(input).toHaveStyle('color: red')
})
it('applies large size variant correctly', () => {
render(<Input size={'large' as any} />)
const input = screen.getByPlaceholderText('Please input')
expect(input.className).toContain(inputVariants({ size: 'large' }))
})
it('uses custom placeholder when provided', () => {
const placeholder = 'Custom placeholder'
render(<Input placeholder={placeholder} />)
const input = screen.getByPlaceholderText(placeholder)
expect(input).toBeInTheDocument()
})
})

View File

@ -43,7 +43,7 @@ const Input = ({
styleCss,
value,
placeholder,
onChange,
onChange = () => { },
unit,
...props
}: InputProps) => {

View File

@ -0,0 +1,29 @@
import React from 'react'
import { render } from '@testing-library/react'
import '@testing-library/jest-dom'
import Loading from './index'
describe('Loading Component', () => {
it('renders correctly with default props', () => {
const { container } = render(<Loading />)
expect(container.firstChild).toHaveClass('flex w-full items-center justify-center')
expect(container.firstChild).not.toHaveClass('h-full')
})
it('renders correctly with area type', () => {
const { container } = render(<Loading type="area" />)
expect(container.firstChild).not.toHaveClass('h-full')
})
it('renders correctly with app type', () => {
const { container } = render(<Loading type='app' />)
expect(container.firstChild).toHaveClass('h-full')
})
it('contains SVG with spin-animation class', () => {
const { container } = render(<Loading />)
const svgElement = container.querySelector('svg')
expect(svgElement).toHaveClass('spin-animation')
})
})

View File

@ -0,0 +1,121 @@
import React from 'react'
import { cleanup, fireEvent, render } from '@testing-library/react'
import '@testing-library/jest-dom'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '.'
afterEach(cleanup)
describe('PortalToFollowElem', () => {
describe('Context and Provider', () => {
test('should throw error when using context outside provider', () => {
// Suppress console.error for this test
const originalError = console.error
console.error = jest.fn()
expect(() => {
render(
<PortalToFollowElemTrigger>Trigger </PortalToFollowElemTrigger>,
)
}).toThrow('PortalToFollowElem components must be wrapped in <PortalToFollowElem />')
console.error = originalError
})
test('should not throw when used within provider', () => {
expect(() => {
render(
<PortalToFollowElem>
<PortalToFollowElemTrigger>Trigger </PortalToFollowElemTrigger>
</PortalToFollowElem>,
)
}).not.toThrow()
})
})
describe('PortalToFollowElemTrigger', () => {
test('should render children correctly', () => {
const { getByText } = render(
<PortalToFollowElem>
<PortalToFollowElemTrigger>Trigger Text </PortalToFollowElemTrigger>
</PortalToFollowElem>,
)
expect(getByText('Trigger Text')).toBeInTheDocument()
})
test('should handle asChild prop correctly', () => {
const { getByRole } = render(
<PortalToFollowElem>
<PortalToFollowElemTrigger asChild >
<button>Button Trigger </button>
</PortalToFollowElemTrigger>
</PortalToFollowElem>,
)
expect(getByRole('button')).toHaveTextContent('Button Trigger')
})
})
describe('PortalToFollowElemContent', () => {
test('should not render content when closed', () => {
const { queryByText } = render(
<PortalToFollowElem open={false} >
<PortalToFollowElemTrigger>Trigger </PortalToFollowElemTrigger>
<PortalToFollowElemContent > Popup Content </PortalToFollowElemContent>
</PortalToFollowElem>,
)
expect(queryByText('Popup Content')).not.toBeInTheDocument()
})
test('should render content when open', () => {
const { getByText } = render(
<PortalToFollowElem open={true} >
<PortalToFollowElemTrigger>Trigger </PortalToFollowElemTrigger>
<PortalToFollowElemContent > Popup Content </PortalToFollowElemContent>
</PortalToFollowElem>,
)
expect(getByText('Popup Content')).toBeInTheDocument()
})
})
describe('Controlled behavior', () => {
test('should call onOpenChange when interaction happens', () => {
const handleOpenChange = jest.fn()
const { getByText } = render(
<PortalToFollowElem onOpenChange={handleOpenChange} >
<PortalToFollowElemTrigger>Hover Me </PortalToFollowElemTrigger>
<PortalToFollowElemContent > Content </PortalToFollowElemContent>
</PortalToFollowElem>,
)
fireEvent.mouseEnter(getByText('Hover Me'))
expect(handleOpenChange).toHaveBeenCalled()
fireEvent.mouseLeave(getByText('Hover Me'))
expect(handleOpenChange).toHaveBeenCalled()
})
})
describe('Configuration options', () => {
test('should accept placement prop', () => {
// Since we can't easily test actual positioning, we'll check if the prop is passed correctly
const useFloatingMock = jest.spyOn(require('@floating-ui/react'), 'useFloating')
render(
<PortalToFollowElem placement="top-start" >
<PortalToFollowElemTrigger>Trigger </PortalToFollowElemTrigger>
</PortalToFollowElem>,
)
expect(useFloatingMock).toHaveBeenCalledWith(
expect.objectContaining({
placement: 'top-start',
}),
)
useFloatingMock.mockRestore()
})
})
})

View File

@ -0,0 +1,49 @@
import React from 'react'
import { render } from '@testing-library/react'
import '@testing-library/jest-dom'
import Spinner from './index'
describe('Spinner component', () => {
it('should render correctly when loading is true', () => {
const { container } = render(<Spinner loading={true} />)
const spinner = container.firstChild as HTMLElement
expect(spinner).toHaveClass('animate-spin')
// Check for accessibility text
const screenReaderText = spinner.querySelector('span')
expect(screenReaderText).toBeInTheDocument()
expect(screenReaderText).toHaveTextContent('Loading...')
})
it('should be hidden when loading is false', () => {
const { container } = render(<Spinner loading={false} />)
const spinner = container.firstChild as HTMLElement
expect(spinner).toHaveClass('hidden')
})
it('should render with custom className', () => {
const customClass = 'text-blue-500'
const { container } = render(<Spinner loading={true} className={customClass} />)
const spinner = container.firstChild as HTMLElement
expect(spinner).toHaveClass(customClass)
})
it('should render children correctly', () => {
const childText = 'Child content'
const { getByText } = render(
<Spinner loading={true}>{childText}</Spinner>,
)
expect(getByText(childText)).toBeInTheDocument()
})
it('should use default loading value (false) when not provided', () => {
const { container } = render(<Spinner />)
const spinner = container.firstChild as HTMLElement
expect(spinner).toHaveClass('hidden')
})
})

View File

@ -0,0 +1,191 @@
import React from 'react'
import { act, render, screen, waitFor } from '@testing-library/react'
import Toast, { ToastProvider, useToastContext } from '.'
import '@testing-library/jest-dom'
// Mock timers for testing timeouts
jest.useFakeTimers()
const TestComponent = () => {
const { notify, close } = useToastContext()
return (
<div>
<button onClick={() => notify({ message: 'Notification message', type: 'info' })}>
Show Toast
</button>
<button onClick={close}>Close Toast</button>
</div>
)
}
describe('Toast', () => {
describe('Toast Component', () => {
test('renders toast with correct type and message', () => {
render(
<ToastProvider>
<Toast type="success" message="Success message" />
</ToastProvider>,
)
expect(screen.getByText('Success message')).toBeInTheDocument()
})
test('renders with different types', () => {
const { rerender } = render(
<ToastProvider>
<Toast type="success" message="Success message" />
</ToastProvider>,
)
expect(document.querySelector('.text-text-success')).toBeInTheDocument()
rerender(
<ToastProvider>
<Toast type="error" message="Error message" />
</ToastProvider>,
)
expect(document.querySelector('.text-text-destructive')).toBeInTheDocument()
})
test('renders with custom component', () => {
render(
<ToastProvider>
<Toast
message="Message with custom component"
customComponent={<span data-testid="custom-component">Custom</span>}
/>
</ToastProvider>,
)
expect(screen.getByTestId('custom-component')).toBeInTheDocument()
})
test('renders children content', () => {
render(
<ToastProvider>
<Toast message="Message with children">
<span>Additional information</span>
</Toast>
</ToastProvider>,
)
expect(screen.getByText('Additional information')).toBeInTheDocument()
})
test('does not render close button when close is undefined', () => {
// Create a modified context where close is undefined
const CustomToastContext = React.createContext({ notify: () => { }, close: undefined })
// Create a wrapper component using the custom context
const Wrapper = ({ children }: any) => (
<CustomToastContext.Provider value={{ notify: () => { }, close: undefined }}>
{children}
</CustomToastContext.Provider>
)
render(
<Wrapper>
<Toast message="No close button" type="info" />
</Wrapper>,
)
expect(screen.getByText('No close button')).toBeInTheDocument()
// Ensure the close button is not rendered
expect(document.querySelector('.h-4.w-4.shrink-0.text-text-tertiary')).not.toBeInTheDocument()
})
})
describe('ToastProvider and Context', () => {
test('shows and hides toast using context', async () => {
render(
<ToastProvider>
<TestComponent />
</ToastProvider>,
)
// No toast initially
expect(screen.queryByText('Notification message')).not.toBeInTheDocument()
// Show toast
act(() => {
screen.getByText('Show Toast').click()
})
expect(screen.getByText('Notification message')).toBeInTheDocument()
// Close toast
act(() => {
screen.getByText('Close Toast').click()
})
expect(screen.queryByText('Notification message')).not.toBeInTheDocument()
})
test('automatically hides toast after duration', async () => {
render(
<ToastProvider>
<TestComponent />
</ToastProvider>,
)
// Show toast
act(() => {
screen.getByText('Show Toast').click()
})
expect(screen.getByText('Notification message')).toBeInTheDocument()
// Fast-forward timer
act(() => {
jest.advanceTimersByTime(3000) // Default for info type is 3000ms
})
// Toast should be gone
await waitFor(() => {
expect(screen.queryByText('Notification message')).not.toBeInTheDocument()
})
})
})
describe('Toast.notify static method', () => {
test('creates and removes toast from DOM', async () => {
act(() => {
// Call the static method
Toast.notify({ message: 'Static notification', type: 'warning' })
})
// Toast should be in document
expect(screen.getByText('Static notification')).toBeInTheDocument()
// Fast-forward timer
act(() => {
jest.advanceTimersByTime(6000) // Default for warning type is 6000ms
})
// Toast should be removed
await waitFor(() => {
expect(screen.queryByText('Static notification')).not.toBeInTheDocument()
})
})
test('calls onClose callback after duration', async () => {
const onCloseMock = jest.fn()
act(() => {
Toast.notify({
message: 'Closing notification',
type: 'success',
onClose: onCloseMock,
})
})
// Fast-forward timer
act(() => {
jest.advanceTimersByTime(3000) // Default for success type is 3000ms
})
// onClose should be called
await waitFor(() => {
expect(onCloseMock).toHaveBeenCalled()
})
})
})
})

View File

@ -0,0 +1,116 @@
import React from 'react'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import Tooltip from './index'
afterEach(cleanup)
describe('Tooltip', () => {
describe('Rendering', () => {
test('should render default tooltip with question icon', () => {
const triggerClassName = 'custom-trigger'
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} />)
const trigger = container.querySelector(`.${triggerClassName}`)
expect(trigger).not.toBeNull()
expect(trigger?.querySelector('svg')).not.toBeNull() // question icon
})
test('should render with custom children', () => {
const { getByText } = render(
<Tooltip popupContent="Tooltip content">
<button>Hover me</button>
</Tooltip>,
)
expect(getByText('Hover me').textContent).toBe('Hover me')
})
})
describe('Disabled state', () => {
test('should not show tooltip when disabled', () => {
const triggerClassName = 'custom-trigger'
const { container } = render(<Tooltip popupContent="Tooltip content" disabled triggerClassName={triggerClassName} />)
const trigger = container.querySelector(`.${triggerClassName}`)
act(() => {
fireEvent.mouseEnter(trigger!)
})
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
})
})
describe('Trigger methods', () => {
test('should open on hover when triggerMethod is hover', () => {
const triggerClassName = 'custom-trigger'
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} />)
const trigger = container.querySelector(`.${triggerClassName}`)
act(() => {
fireEvent.mouseEnter(trigger!)
})
expect(screen.queryByText('Tooltip content')).toBeInTheDocument()
})
test('should close on mouse leave when triggerMethod is hover', () => {
const triggerClassName = 'custom-trigger'
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} />)
const trigger = container.querySelector(`.${triggerClassName}`)
act(() => {
fireEvent.mouseEnter(trigger!)
fireEvent.mouseLeave(trigger!)
})
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
})
test('should toggle on click when triggerMethod is click', () => {
const triggerClassName = 'custom-trigger'
const { container } = render(<Tooltip popupContent="Tooltip content" triggerMethod="click" triggerClassName={triggerClassName} />)
const trigger = container.querySelector(`.${triggerClassName}`)
act(() => {
fireEvent.click(trigger!)
})
expect(screen.queryByText('Tooltip content')).toBeInTheDocument()
})
test('should not close immediately on mouse leave when needsDelay is true', () => {
const triggerClassName = 'custom-trigger'
const { container } = render(<Tooltip popupContent="Tooltip content" triggerMethod="hover" needsDelay triggerClassName={triggerClassName} />)
const trigger = container.querySelector(`.${triggerClassName}`)
act(() => {
fireEvent.mouseEnter(trigger!)
fireEvent.mouseLeave(trigger!)
})
expect(screen.queryByText('Tooltip content')).toBeInTheDocument()
})
})
describe('Styling and positioning', () => {
test('should apply custom trigger className', () => {
const triggerClassName = 'custom-trigger'
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} />)
const trigger = container.querySelector(`.${triggerClassName}`)
expect(trigger?.className).toContain('custom-trigger')
})
test('should apply custom popup className', async () => {
const triggerClassName = 'custom-trigger'
const { container } = render(<Tooltip popupContent="Tooltip content" triggerClassName={triggerClassName} popupClassName="custom-popup" />)
const trigger = container.querySelector(`.${triggerClassName}`)
act(() => {
fireEvent.mouseEnter(trigger!)
})
expect((await screen.findByText('Tooltip content'))?.className).toContain('custom-popup')
})
test('should apply noDecoration when specified', async () => {
const triggerClassName = 'custom-trigger'
const { container } = render(<Tooltip
popupContent="Tooltip content"
triggerClassName={triggerClassName}
noDecoration
/>)
const trigger = container.querySelector(`.${triggerClassName}`)
act(() => {
fireEvent.mouseEnter(trigger!)
})
expect((await screen.findByText('Tooltip content'))?.className).not.toContain('bg-components-panel-bg')
})
})
})

View File

@ -26,7 +26,7 @@ const config: Config = {
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: false,
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,