| @@ -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 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) | |||
| }) | |||
| 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('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) | |||
| }) | |||
| }) | |||
| test('Button should have primary variant', async () => { | |||
| const { getByRole } = render(<Button variant='primary'>Click me</Button>) | |||
| expect(getByRole('button').className).toContain('btn-primary') | |||
| 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') | |||
| }) | |||
| }) | |||
| test('Button should have warning variant', async () => { | |||
| const { getByRole } = render(<Button variant='warning'>Click me</Button>) | |||
| expect(getByRole('button').className).toContain('btn-warning') | |||
| 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') | |||
| }) | |||
| }) | |||
| 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 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() | |||
| 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() | |||
| }) | |||
| }) | |||
| }) | |||
| @@ -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') | |||
| }) | |||
| }) | |||
| @@ -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 }), | |||
| ) | |||
| }) | |||
| }) | |||
| @@ -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') | |||
| }) | |||
| }) | |||
| }) | |||
| @@ -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) => { | |||
| @@ -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') | |||
| }) | |||
| }) | |||
| @@ -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() | |||
| }) | |||
| }) | |||
| }) | |||
| @@ -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') | |||
| }) | |||
| }) | |||
| @@ -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() | |||
| }) | |||
| }) | |||
| }) | |||
| }) | |||
| @@ -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, | |||