mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-20 18:59:07 +08:00
chore: add unit test to high frequency component (#17423)
This commit is contained in:
parent
2e9997110a
commit
31a6aabfe5
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
55
web/app/components/base/divider/index.spec.tsx
Normal file
55
web/app/components/base/divider/index.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
67
web/app/components/base/icons/IconBase.spec.tsx
Normal file
67
web/app/components/base/icons/IconBase.spec.tsx
Normal 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 }),
|
||||
)
|
||||
})
|
||||
})
|
70
web/app/components/base/icons/utils.spec.ts
Normal file
70
web/app/components/base/icons/utils.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
124
web/app/components/base/input/index.spec.tsx
Normal file
124
web/app/components/base/input/index.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
@ -43,7 +43,7 @@ const Input = ({
|
||||
styleCss,
|
||||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
onChange = () => { },
|
||||
unit,
|
||||
...props
|
||||
}: InputProps) => {
|
||||
|
29
web/app/components/base/loading/index.spec.tsx
Normal file
29
web/app/components/base/loading/index.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
121
web/app/components/base/portal-to-follow-elem/index.spec.tsx
Normal file
121
web/app/components/base/portal-to-follow-elem/index.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
49
web/app/components/base/spinner/index.spec.tsx
Normal file
49
web/app/components/base/spinner/index.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
191
web/app/components/base/toast/index.spec.tsx
Normal file
191
web/app/components/base/toast/index.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
116
web/app/components/base/tooltip/index.spec.tsx
Normal file
116
web/app/components/base/tooltip/index.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user