diff --git a/web/app/components/base/badge/index.spec.tsx b/web/app/components/base/badge/index.spec.tsx new file mode 100644 index 0000000000..74162841cf --- /dev/null +++ b/web/app/components/base/badge/index.spec.tsx @@ -0,0 +1,360 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import Badge, { BadgeState, BadgeVariants } from './index' + +describe('Badge', () => { + describe('Rendering', () => { + it('should render as a div element with badge class', () => { + render(Test Badge) + + const badge = screen.getByText('Test Badge') + expect(badge).toHaveClass('badge') + expect(badge.tagName).toBe('DIV') + }) + + it.each([ + { children: undefined, label: 'no children' }, + { children: '', label: 'empty string' }, + ])('should render correctly when provided $label', ({ children }) => { + const { container } = render({children}) + + expect(container.firstChild).toHaveClass('badge') + }) + + it('should render React Node children correctly', () => { + render( + + 🔔 + , + ) + + expect(screen.getByTestId('badge-with-icon')).toBeInTheDocument() + expect(screen.getByTestId('custom-icon')).toBeInTheDocument() + }) + }) + + describe('size prop', () => { + it.each([ + { size: undefined, label: 'medium (default)' }, + { size: 's', label: 'small' }, + { size: 'm', label: 'medium' }, + { size: 'l', label: 'large' }, + ] as const)('should render with $label size', ({ size }) => { + render(Test) + + const expectedSize = size || 'm' + expect(screen.getByText('Test')).toHaveClass('badge', `badge-${expectedSize}`) + }) + }) + + describe('state prop', () => { + it.each([ + { state: BadgeState.Warning, label: 'warning', expectedClass: 'badge-warning' }, + { state: BadgeState.Accent, label: 'accent', expectedClass: 'badge-accent' }, + ])('should render with $label state', ({ state, expectedClass }) => { + render(State Test) + + expect(screen.getByText('State Test')).toHaveClass(expectedClass) + }) + + it.each([ + { state: undefined, label: 'default (undefined)' }, + { state: BadgeState.Default, label: 'default (explicit)' }, + ])('should use default styles when state is $label', ({ state }) => { + render(State Test) + + const badge = screen.getByText('State Test') + expect(badge).not.toHaveClass('badge-warning', 'badge-accent') + }) + }) + + describe('iconOnly prop', () => { + it.each([ + { size: 's', iconOnly: false, label: 'small with text' }, + { size: 's', iconOnly: true, label: 'small icon-only' }, + { size: 'm', iconOnly: false, label: 'medium with text' }, + { size: 'm', iconOnly: true, label: 'medium icon-only' }, + { size: 'l', iconOnly: false, label: 'large with text' }, + { size: 'l', iconOnly: true, label: 'large icon-only' }, + ] as const)('should render correctly for $label', ({ size, iconOnly }) => { + const { container } = render(🔔) + const badge = screen.getByText('🔔') + + // Verify badge renders with correct size + expect(badge).toHaveClass('badge', `badge-${size}`) + + // Verify the badge is in the DOM and contains the content + expect(badge).toBeInTheDocument() + expect(container.firstChild).toBe(badge) + }) + + it('should apply icon-only padding when iconOnly is true', () => { + render(🔔) + + // When iconOnly is true, the badge should have uniform padding (all sides equal) + const badge = screen.getByText('🔔') + expect(badge).toHaveClass('p-1') + }) + + it('should apply asymmetric padding when iconOnly is false', () => { + render(Badge) + + // When iconOnly is false, the badge should have different horizontal and vertical padding + const badge = screen.getByText('Badge') + expect(badge).toHaveClass('px-[5px]', 'py-[2px]') + }) + }) + + describe('uppercase prop', () => { + it.each([ + { uppercase: undefined, label: 'default (undefined)', expected: 'system-2xs-medium' }, + { uppercase: false, label: 'explicitly false', expected: 'system-2xs-medium' }, + { uppercase: true, label: 'true', expected: 'system-2xs-medium-uppercase' }, + ])('should apply $expected class when uppercase is $label', ({ uppercase, expected }) => { + render(Text) + + expect(screen.getByText('Text')).toHaveClass(expected) + }) + }) + + describe('styleCss prop', () => { + it('should apply custom inline styles correctly', () => { + const customStyles = { + backgroundColor: 'rgb(0, 0, 255)', + color: 'rgb(255, 255, 255)', + padding: '10px', + } + render(Styled Badge) + + expect(screen.getByText('Styled Badge')).toHaveStyle(customStyles) + }) + + it('should apply inline styles without overriding core classes', () => { + render(Custom) + + const badge = screen.getByText('Custom') + expect(badge).toHaveStyle({ backgroundColor: 'rgb(255, 0, 0)', margin: '5px' }) + expect(badge).toHaveClass('badge') + }) + }) + + describe('className prop', () => { + it.each([ + { + props: { className: 'custom-badge' }, + expected: ['badge', 'custom-badge'], + label: 'single custom class', + }, + { + props: { className: 'custom-class another-class', size: 'l' as const }, + expected: ['badge', 'badge-l', 'custom-class', 'another-class'], + label: 'multiple classes with size variant', + }, + ])('should merge $label with default classes', ({ props, expected }) => { + render(Test) + + expect(screen.getByText('Test')).toHaveClass(...expected) + }) + }) + + describe('HTML attributes passthrough', () => { + it.each([ + { attr: 'data-testid', value: 'custom-badge-id', label: 'data attribute' }, + { attr: 'id', value: 'unique-badge', label: 'id attribute' }, + { attr: 'aria-label', value: 'Notification badge', label: 'aria-label' }, + { attr: 'title', value: 'Hover tooltip', label: 'title attribute' }, + { attr: 'role', value: 'status', label: 'ARIA role' }, + ])('should pass through $label correctly', ({ attr, value }) => { + render(Test) + + expect(screen.getByText('Test')).toHaveAttribute(attr, value) + }) + + it('should support multiple HTML attributes simultaneously', () => { + render( + + Test + , + ) + + const badge = screen.getByTestId('multi-attr-badge') + expect(badge).toHaveAttribute('id', 'badge-123') + expect(badge).toHaveAttribute('aria-label', 'Status indicator') + expect(badge).toHaveAttribute('title', 'Current status') + }) + }) + + describe('Event handlers', () => { + it.each([ + { handler: 'onClick', trigger: fireEvent.click, label: 'click' }, + { handler: 'onMouseEnter', trigger: fireEvent.mouseEnter, label: 'mouse enter' }, + { handler: 'onMouseLeave', trigger: fireEvent.mouseLeave, label: 'mouse leave' }, + ])('should trigger $handler when $label occurs', ({ handler, trigger }) => { + const mockHandler = vi.fn() + render(Badge) + + trigger(screen.getByText('Badge')) + + expect(mockHandler).toHaveBeenCalledTimes(1) + }) + + it('should handle user interaction flow with multiple events', () => { + const handlers = { + onClick: vi.fn(), + onMouseEnter: vi.fn(), + onMouseLeave: vi.fn(), + } + render(Interactive) + + const badge = screen.getByText('Interactive') + fireEvent.mouseEnter(badge) + fireEvent.click(badge) + fireEvent.mouseLeave(badge) + + expect(handlers.onMouseEnter).toHaveBeenCalledTimes(1) + expect(handlers.onClick).toHaveBeenCalledTimes(1) + expect(handlers.onMouseLeave).toHaveBeenCalledTimes(1) + }) + + it('should pass event object to handler with correct properties', () => { + const handleClick = vi.fn() + render(Event Badge) + + fireEvent.click(screen.getByText('Event Badge')) + + expect(handleClick).toHaveBeenCalledWith(expect.objectContaining({ + type: 'click', + })) + }) + }) + + describe('Combined props', () => { + it('should correctly apply all props when used together', () => { + render( + + Full Featured + , + ) + + const badge = screen.getByTestId('combined-badge') + expect(badge).toHaveClass('badge', 'badge-l', 'badge-warning', 'system-2xs-medium-uppercase', 'custom-badge') + expect(badge).toHaveStyle({ backgroundColor: 'rgb(0, 0, 255)' }) + expect(badge).toHaveTextContent('Full Featured') + }) + + it.each([ + { + props: { size: 'l' as const, state: BadgeState.Accent }, + expected: ['badge', 'badge-l', 'badge-accent'], + label: 'size and state variants', + }, + { + props: { iconOnly: true, uppercase: true }, + expected: ['badge', 'system-2xs-medium-uppercase'], + label: 'iconOnly and uppercase', + }, + ])('should combine $label correctly', ({ props, expected }) => { + render(Test) + + expect(screen.getByText('Test')).toHaveClass(...expected) + }) + + it('should handle event handlers with combined props', () => { + const handleClick = vi.fn() + render( + + Test + , + ) + + const badge = screen.getByText('Test') + expect(badge).toHaveClass('badge', 'badge-s', 'badge-warning', 'interactive') + + fireEvent.click(badge) + expect(handleClick).toHaveBeenCalledTimes(1) + }) + }) + + describe('Edge cases', () => { + it.each([ + { children: 42, text: '42', label: 'numeric value' }, + { children: 0, text: '0', label: 'zero' }, + ])('should render $label correctly', ({ children, text }) => { + render({children}) + + expect(screen.getByText(text)).toBeInTheDocument() + }) + + it.each([ + { children: null, label: 'null' }, + { children: false, label: 'boolean false' }, + ])('should handle $label children without errors', ({ children }) => { + const { container } = render({children}) + + expect(container.firstChild).toHaveClass('badge') + }) + + it('should render complex nested content correctly', () => { + render( + + 🔔 + 5 + , + ) + + expect(screen.getByTestId('icon')).toBeInTheDocument() + expect(screen.getByTestId('count')).toBeInTheDocument() + }) + }) + + describe('Component metadata and exports', () => { + it('should have correct displayName for debugging', () => { + expect(Badge.displayName).toBe('Badge') + }) + + describe('BadgeState enum', () => { + it.each([ + { key: 'Warning', value: 'warning' }, + { key: 'Accent', value: 'accent' }, + { key: 'Default', value: '' }, + ])('should export $key state with value "$value"', ({ key, value }) => { + expect(BadgeState[key as keyof typeof BadgeState]).toBe(value) + }) + }) + + describe('BadgeVariants utility', () => { + it('should be a function', () => { + expect(typeof BadgeVariants).toBe('function') + }) + + it('should generate base badge class with default medium size', () => { + const result = BadgeVariants({}) + + expect(result).toContain('badge') + expect(result).toContain('badge-m') + }) + + it.each([ + { size: 's' }, + { size: 'm' }, + { size: 'l' }, + ] as const)('should generate correct classes for size=$size', ({ size }) => { + const result = BadgeVariants({ size }) + + expect(result).toContain('badge') + expect(result).toContain(`badge-${size}`) + }) + }) + }) +})