chore: add unit test to high frequency component (#17423)
This commit is contained in:
@@ -4,20 +4,33 @@ import Button from './index'
|
|||||||
|
|
||||||
afterEach(cleanup)
|
afterEach(cleanup)
|
||||||
// https://testing-library.com/docs/queries/about
|
// https://testing-library.com/docs/queries/about
|
||||||
describe('Button text', () => {
|
describe('Button', () => {
|
||||||
|
describe('Button text', () => {
|
||||||
test('Button text should be same as children', async () => {
|
test('Button text should be same as children', async () => {
|
||||||
const { getByRole, container } = render(<Button>Click me</Button>)
|
const { getByRole, container } = render(<Button>Click me</Button>)
|
||||||
expect(getByRole('button').textContent).toBe('Click me')
|
expect(getByRole('button').textContent).toBe('Click me')
|
||||||
expect(container.querySelector('button')?.textContent).toBe('Click me')
|
expect(container.querySelector('button')?.textContent).toBe('Click me')
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Button loading', () => {
|
||||||
test('Loading button text should include same as children', async () => {
|
test('Loading button text should include same as children', async () => {
|
||||||
const { getByRole } = render(<Button loading>Click me</Button>)
|
const { getByRole } = render(<Button loading>Click me</Button>)
|
||||||
expect(getByRole('button').textContent?.includes('Loading')).toBe(true)
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
describe('Button style', () => {
|
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 () => {
|
test('Button should have default variant', async () => {
|
||||||
const { getByRole } = render(<Button>Click me</Button>)
|
const { getByRole } = render(<Button>Click me</Button>)
|
||||||
expect(getByRole('button').className).toContain('btn-secondary')
|
expect(getByRole('button').className).toContain('btn-secondary')
|
||||||
@@ -33,17 +46,65 @@ describe('Button style', () => {
|
|||||||
expect(getByRole('button').className).toContain('btn-warning')
|
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 () => {
|
test('Button disabled should have disabled variant', async () => {
|
||||||
const { getByRole } = render(<Button disabled>Click me</Button>)
|
const { getByRole } = render(<Button disabled>Click me</Button>)
|
||||||
expect(getByRole('button').className).toContain('btn-disabled')
|
expect(getByRole('button').className).toContain('btn-disabled')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Button events', () => {
|
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 () => {
|
test('onClick should been call after clicked', async () => {
|
||||||
const onClick = jest.fn()
|
const onClick = jest.fn()
|
||||||
const { getByRole } = render(<Button onClick={onClick}>Click me</Button>)
|
const { getByRole } = render(<Button onClick={onClick}>Click me</Button>)
|
||||||
fireEvent.click(getByRole('button'))
|
fireEvent.click(getByRole('button'))
|
||||||
expect(onClick).toHaveBeenCalled()
|
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,
|
styleCss,
|
||||||
value,
|
value,
|
||||||
placeholder,
|
placeholder,
|
||||||
onChange,
|
onChange = () => { },
|
||||||
unit,
|
unit,
|
||||||
...props
|
...props
|
||||||
}: InputProps) => {
|
}: 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,
|
clearMocks: true,
|
||||||
|
|
||||||
// Indicates whether the coverage information should be collected while executing the test
|
// 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
|
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||||
// collectCoverageFrom: undefined,
|
// collectCoverageFrom: undefined,
|
||||||
|
Reference in New Issue
Block a user