test: add unit tests for base components-part-1 (#32154)

This commit is contained in:
Poojan 2026-02-13 08:44:14 +05:30 committed by GitHub
parent f3f56f03e3
commit 84d090db33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1154 additions and 0 deletions

View File

@ -0,0 +1,59 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ContentDialog from './index'
describe('ContentDialog', () => {
it('renders children when show is true', async () => {
render(
<ContentDialog show={true}>
<div>Dialog body</div>
</ContentDialog>,
)
await screen.findByText('Dialog body')
expect(screen.getByText('Dialog body')).toBeInTheDocument()
const backdrop = document.querySelector('.bg-app-detail-overlay-bg')
expect(backdrop).toBeTruthy()
})
it('does not render children when show is false', () => {
render(
<ContentDialog show={false}>
<div>Hidden content</div>
</ContentDialog>,
)
expect(screen.queryByText('Hidden content')).toBeNull()
expect(document.querySelector('.bg-app-detail-overlay-bg')).toBeNull()
})
it('calls onClose when backdrop is clicked', async () => {
const onClose = vi.fn()
render(
<ContentDialog show={true} onClose={onClose}>
<div>Body</div>
</ContentDialog>,
)
const user = userEvent.setup()
const backdrop = document.querySelector('.bg-app-detail-overlay-bg') as HTMLElement | null
expect(backdrop).toBeTruthy()
await user.click(backdrop!)
expect(onClose).toHaveBeenCalledTimes(1)
})
it('applies provided className to the content panel', () => {
render(
<ContentDialog show={true} className="my-panel-class">
<div>Panel content</div>
</ContentDialog>,
)
const contentPanel = document.querySelector('.bg-app-detail-bg') as HTMLElement | null
expect(contentPanel).toBeTruthy()
expect(contentPanel?.className).toContain('my-panel-class')
expect(screen.getByText('Panel content')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,138 @@
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import CustomDialog from './index'
describe('CustomDialog Component', () => {
const setup = () => userEvent.setup()
it('should render children and title when show is true', async () => {
render(
<CustomDialog show={true} title="Modal Title">
<div data-testid="dialog-content">Main Content</div>
</CustomDialog>,
)
const title = await screen.findByText('Modal Title')
const content = screen.getByTestId('dialog-content')
expect(title).toBeInTheDocument()
expect(content).toBeInTheDocument()
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should not render anything when show is false', async () => {
render(
<CustomDialog show={false} title="Hidden Title">
<div>Content</div>
</CustomDialog>,
)
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
expect(screen.queryByText('Hidden Title')).not.toBeInTheDocument()
})
it('should apply the correct semantic tag to title using titleAs', async () => {
render(
<CustomDialog show={true} title="Semantic Title" titleAs="h1">
Content
</CustomDialog>,
)
const title = await screen.findByRole('heading', { level: 1 })
expect(title).toHaveTextContent('Semantic Title')
})
it('should render the footer only when the prop is provided', async () => {
const { rerender } = render(
<CustomDialog show={true}>Content</CustomDialog>,
)
await screen.findByRole('dialog')
expect(screen.queryByText('Footer Content')).not.toBeInTheDocument()
rerender(
<CustomDialog show={true} footer={<div data-testid="footer-node">Footer Content</div>}>
Content
</CustomDialog>,
)
expect(await screen.findByTestId('footer-node')).toBeInTheDocument()
})
it('should call onClose when Escape key is pressed', async () => {
const user = setup()
const onCloseMock = vi.fn()
render(
<CustomDialog show={true} onClose={onCloseMock}>
Content
</CustomDialog>,
)
await screen.findByRole('dialog')
await act(async () => {
await user.keyboard('{Escape}')
})
expect(onCloseMock).toHaveBeenCalledTimes(1)
})
it('should call onClose when the backdrop is clicked', async () => {
const user = setup()
const onCloseMock = vi.fn()
render(
<CustomDialog show={true} onClose={onCloseMock}>
Content
</CustomDialog>,
)
await screen.findByRole('dialog')
const backdrop = document.querySelector('.bg-background-overlay-backdrop')
expect(backdrop).toBeInTheDocument()
await act(async () => {
await user.click(backdrop!)
})
expect(onCloseMock).toHaveBeenCalledTimes(1)
})
it('should apply custom class names to internal elements', async () => {
render(
<CustomDialog
show={true}
title="Title"
className="custom-panel-container"
titleClassName="custom-title-style"
bodyClassName="custom-body-style"
footer="Footer"
footerClassName="custom-footer-style"
>
<div data-testid="content">Content</div>
</CustomDialog>,
)
await screen.findByRole('dialog')
expect(document.querySelector('.custom-panel-container')).toBeInTheDocument()
expect(document.querySelector('.custom-title-style')).toBeInTheDocument()
expect(document.querySelector('.custom-body-style')).toBeInTheDocument()
expect(document.querySelector('.custom-footer-style')).toBeInTheDocument()
})
it('should maintain accessibility attributes (aria-modal)', async () => {
render(
<CustomDialog show={true} title="Accessibility Test">
<button>Focusable Item</button>
</CustomDialog>,
)
const dialog = await screen.findByRole('dialog')
// Headless UI should automatically set aria-modal="true"
expect(dialog).toHaveAttribute('aria-modal', 'true')
})
})

View File

@ -0,0 +1,214 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import FullScreenModal from './index'
describe('FullScreenModal Component', () => {
it('should not render anything when open is false', () => {
render(
<FullScreenModal open={false}>
<div data-testid="modal-content">Content</div>
</FullScreenModal>,
)
expect(screen.queryByTestId('modal-content')).not.toBeInTheDocument()
})
it('should render content when open is true', async () => {
render(
<FullScreenModal open={true}>
<div data-testid="modal-content">Content</div>
</FullScreenModal>,
)
expect(await screen.findByTestId('modal-content')).toBeInTheDocument()
})
it('should not crash when provided with title and description props', async () => {
await act(async () => {
render(
<FullScreenModal
open={true}
title="My Title"
description="My Description"
>
Content
</FullScreenModal>,
)
})
})
describe('Props Handling', () => {
it('should apply wrapperClassName to the dialog root', async () => {
render(
<FullScreenModal
open={true}
wrapperClassName="custom-wrapper-class"
>
Content
</FullScreenModal>,
)
await screen.findByRole('dialog')
const element = document.querySelector('.custom-wrapper-class')
expect(element).toBeInTheDocument()
expect(element).toHaveClass('modal-dialog')
})
it('should apply className to the inner panel', async () => {
await act(async () => {
render(
<FullScreenModal
open={true}
className="custom-panel-class"
>
Content
</FullScreenModal>,
)
})
const panel = document.querySelector('.custom-panel-class')
expect(panel).toBeInTheDocument()
expect(panel).toHaveClass('h-full')
})
it('should handle overflowVisible prop', async () => {
const { rerender } = await act(async () => {
return render(
<FullScreenModal
open={true}
overflowVisible={true}
className="target-panel"
>
Content
</FullScreenModal>,
)
})
let panel = document.querySelector('.target-panel')
expect(panel).toHaveClass('overflow-visible')
expect(panel).not.toHaveClass('overflow-hidden')
await act(async () => {
rerender(
<FullScreenModal
open={true}
overflowVisible={false}
className="target-panel"
>
Content
</FullScreenModal>,
)
})
panel = document.querySelector('.target-panel')
expect(panel).toHaveClass('overflow-hidden')
expect(panel).not.toHaveClass('overflow-visible')
})
it('should render close button when closable is true', async () => {
await act(async () => {
render(
<FullScreenModal open={true} closable={true}>
Content
</FullScreenModal>,
)
})
const closeButton = document.querySelector('.bg-components-button-tertiary-bg')
expect(closeButton).toBeInTheDocument()
})
it('should not render close button when closable is false', async () => {
await act(async () => {
render(
<FullScreenModal open={true} closable={false}>
Content
</FullScreenModal>,
)
})
const closeButton = document.querySelector('.bg-components-button-tertiary-bg')
expect(closeButton).not.toBeInTheDocument()
})
})
describe('Interactions', () => {
it('should call onClose when close button is clicked', async () => {
const user = userEvent.setup()
const onClose = vi.fn()
render(
<FullScreenModal open={true} closable={true} onClose={onClose}>
Content
</FullScreenModal>,
)
const closeBtn = document.querySelector('.bg-components-button-tertiary-bg')
expect(closeBtn).toBeInTheDocument()
await user.click(closeBtn!)
expect(onClose).toHaveBeenCalledTimes(1)
})
it('should call onClose when clicking the backdrop', async () => {
const user = userEvent.setup()
const onClose = vi.fn()
render(
<FullScreenModal open={true} onClose={onClose}>
<div data-testid="inner">Content</div>
</FullScreenModal>,
)
const dialog = document.querySelector('.modal-dialog')
if (dialog) {
await user.click(dialog)
expect(onClose).toHaveBeenCalled()
}
else {
throw new Error('Dialog root not found')
}
})
it('should call onClose when Escape key is pressed', async () => {
const user = userEvent.setup()
const onClose = vi.fn()
render(
<FullScreenModal open={true} onClose={onClose}>
Content
</FullScreenModal>,
)
await user.keyboard('{Escape}')
expect(onClose).toHaveBeenCalled()
})
it('should not call onClose when clicking inside the content', async () => {
const user = userEvent.setup()
const onClose = vi.fn()
render(
<FullScreenModal open={true} onClose={onClose}>
<div className="bg-background-default-subtle">
<button>Action</button>
</div>
</FullScreenModal>,
)
const innerButton = screen.getByRole('button', { name: 'Action' })
await user.click(innerButton)
expect(onClose).not.toHaveBeenCalled()
const contentPanel = document.querySelector('.bg-background-default-subtle')
await act(async () => {
fireEvent.click(contentPanel!)
})
expect(onClose).not.toHaveBeenCalled()
})
})
describe('Default Props', () => {
it('should not throw if onClose is not provided', async () => {
const user = userEvent.setup()
render(<FullScreenModal open={true} closable={true}>Content</FullScreenModal>)
const closeButton = document.querySelector('.bg-components-button-tertiary-bg')
await user.click(closeButton!)
})
})
})

View File

@ -0,0 +1,205 @@
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import i18next from 'i18next'
import { useParams, usePathname } from 'next/navigation'
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import AudioBtn from './index'
const mockPlayAudio = vi.fn()
const mockPauseAudio = vi.fn()
const mockGetAudioPlayer = vi.fn()
vi.mock('next/navigation', () => ({
useParams: vi.fn(),
usePathname: vi.fn(),
}))
vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({
AudioPlayerManager: {
getInstance: vi.fn(() => ({
getAudioPlayer: mockGetAudioPlayer,
})),
},
}))
describe('AudioBtn', () => {
const getButton = () => screen.getByRole('button')
const hoverAndCheckTooltip = async (expectedText: string) => {
const button = getButton()
await userEvent.hover(button)
expect(await screen.findByText(expectedText)).toBeInTheDocument()
}
const getAudioCallback = () => {
const lastCall = mockGetAudioPlayer.mock.calls[mockGetAudioPlayer.mock.calls.length - 1]
const callback = lastCall?.find((arg: unknown) => typeof arg === 'function') as ((event: string) => void) | undefined
if (!callback)
throw new Error('Audio callback not found - ensure mockGetAudioPlayer was called with a callback argument')
return callback
}
beforeAll(() => {
i18next.init({})
})
beforeEach(() => {
vi.clearAllMocks()
mockGetAudioPlayer.mockReturnValue({
playAudio: mockPlayAudio,
pauseAudio: mockPauseAudio,
})
; (useParams as ReturnType<typeof vi.fn>).mockReturnValue({})
; (usePathname as ReturnType<typeof vi.fn>).mockReturnValue('/')
})
describe('URL Routing', () => {
it('should generate public URL when token is present', async () => {
; (useParams as ReturnType<typeof vi.fn>).mockReturnValue({ token: 'test-token' })
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
expect(mockGetAudioPlayer.mock.calls[0][0]).toBe('/text-to-audio')
expect(mockGetAudioPlayer.mock.calls[0][1]).toBe(true)
})
it('should generate app URL when appId is present', async () => {
; (useParams as ReturnType<typeof vi.fn>).mockReturnValue({ appId: '123' })
; (usePathname as ReturnType<typeof vi.fn>).mockReturnValue('/apps/123/chat')
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
expect(mockGetAudioPlayer.mock.calls[0][0]).toBe('/apps/123/text-to-audio')
expect(mockGetAudioPlayer.mock.calls[0][1]).toBe(false)
})
it('should generate installed app URL correctly', async () => {
; (useParams as ReturnType<typeof vi.fn>).mockReturnValue({ appId: '456' })
; (usePathname as ReturnType<typeof vi.fn>).mockReturnValue('/explore/installed/app')
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
expect(mockGetAudioPlayer.mock.calls[0][0]).toBe('/installed-apps/456/text-to-audio')
})
})
describe('State Management', () => {
it('should start in initial state', async () => {
render(<AudioBtn value="test" />)
await hoverAndCheckTooltip('play')
expect(getButton()).toHaveClass('action-btn')
expect(getButton()).not.toBeDisabled()
})
it('should transition to playing state', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
act(() => {
getAudioCallback()('play')
})
await hoverAndCheckTooltip('playing')
expect(getButton()).toHaveClass('action-btn-active')
})
it('should transition to ended state', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
act(() => {
getAudioCallback()('play')
})
act(() => {
getAudioCallback()('ended')
})
await hoverAndCheckTooltip('play')
expect(getButton()).not.toHaveClass('action-btn-active')
})
it('should handle paused event', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
act(() => {
getAudioCallback()('play')
})
act(() => {
getAudioCallback()('paused')
})
await hoverAndCheckTooltip('play')
})
it('should handle error event', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
act(() => {
getAudioCallback()('error')
})
await hoverAndCheckTooltip('play')
})
it('should handle loaded event', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
act(() => {
getAudioCallback()('loaded')
})
await hoverAndCheckTooltip('loading')
})
})
describe('Play/Pause', () => {
it('should call playAudio when clicked', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await waitFor(() => expect(mockPlayAudio).toHaveBeenCalled())
})
it('should call pauseAudio when clicked while playing', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
act(() => {
getAudioCallback()('play')
})
await userEvent.click(getButton())
await waitFor(() => expect(mockPauseAudio).toHaveBeenCalled())
})
it('should disable button when loading', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await waitFor(() => expect(getButton()).toBeDisabled())
})
})
describe('Props', () => {
it('should pass props to audio player', async () => {
render(<AudioBtn value="hello" id="msg-1" voice="en-US" />)
await userEvent.click(getButton())
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
const call = mockGetAudioPlayer.mock.calls[0]
expect(call[2]).toBe('msg-1')
expect(call[3]).toBe('hello')
expect(call[4]).toBe('en-US')
})
})
})

View File

@ -0,0 +1,49 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import NotionConnector from './index'
describe('NotionConnector', () => {
it('should render the layout and actual sub-components (Icons & Button)', () => {
const { container } = render(<NotionConnector onSetting={vi.fn()} />)
// Verify Title & Tip translations
expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepOne.notionSyncTip')).toBeInTheDocument()
const notionWrapper = container.querySelector('.h-12.w-12')
const dotsWrapper = container.querySelector('.system-md-semibold')
expect(notionWrapper?.querySelector('svg')).toBeInTheDocument()
expect(dotsWrapper?.querySelector('svg')).toBeInTheDocument()
const button = screen.getByRole('button', {
name: /datasetcreation.stepone.connect/i,
})
expect(button).toBeInTheDocument()
expect(button).toHaveClass('btn', 'btn-primary')
})
it('should trigger the onSetting callback when the real button is clicked', async () => {
const onSetting = vi.fn()
const user = userEvent.setup()
render(<NotionConnector onSetting={onSetting} />)
const button = screen.getByRole('button', {
name: /datasetcreation.stepone.connect/i,
})
await user.click(button)
expect(onSetting).toHaveBeenCalledTimes(1)
})
it('should maintain the correct visual hierarchy classes', () => {
const { container } = render(<NotionConnector onSetting={vi.fn()} />)
// Verify the outer container has the specific workflow-process-bg
const mainContainer = container.firstChild
expect(mainContainer).toHaveClass('bg-workflow-process-bg', 'rounded-2xl', 'p-6')
})
})

View File

@ -0,0 +1,83 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import {
SkeletonContainer,
SkeletonPoint,
SkeletonRectangle,
SkeletonRow,
} from './index'
describe('Skeleton Components', () => {
describe('Individual Components', () => {
it('should forward attributes and render children in SkeletonContainer', () => {
render(
<SkeletonContainer data-testid="container" className="custom-container">
<span>Content</span>
</SkeletonContainer>,
)
const element = screen.getByTestId('container')
expect(element).toHaveClass('flex', 'flex-col', 'custom-container')
expect(screen.getByText('Content')).toBeInTheDocument()
})
it('should forward attributes and render children in SkeletonRow', () => {
render(
<SkeletonRow data-testid="row" className="custom-row">
<span>Row Content</span>
</SkeletonRow>,
)
const element = screen.getByTestId('row')
expect(element).toHaveClass('flex', 'items-center', 'custom-row')
expect(screen.getByText('Row Content')).toBeInTheDocument()
})
it('should apply base skeleton styles to SkeletonRectangle', () => {
render(<SkeletonRectangle data-testid="rect" className="w-10" />)
const element = screen.getByTestId('rect')
expect(element).toHaveClass('h-2', 'bg-text-quaternary', 'opacity-20', 'w-10')
})
it('should render the separator character correctly in SkeletonPoint', () => {
render(<SkeletonPoint data-testid="point" />)
const element = screen.getByTestId('point')
expect(element).toHaveTextContent('·')
expect(element).toHaveClass('text-text-quaternary')
})
})
describe('Composition & Layout', () => {
it('should render a full skeleton structure accurately', () => {
const { container } = render(
<SkeletonContainer className="main-wrapper">
<SkeletonRow>
<SkeletonRectangle className="rect-1" />
<SkeletonPoint />
<SkeletonRectangle className="rect-2" />
</SkeletonRow>
</SkeletonContainer>,
)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('main-wrapper')
expect(container.querySelector('.rect-1')).toBeInTheDocument()
expect(container.querySelector('.rect-2')).toBeInTheDocument()
const row = container.querySelector('.flex.items-center')
expect(row).toContainElement(container.querySelector('.rect-1') as HTMLElement)
expect(row).toHaveTextContent('·')
})
})
it('should handle rest props like event listeners', async () => {
const onClick = vi.fn()
const user = userEvent.setup()
render(<SkeletonRectangle onClick={onClick} data-testid="clickable" />)
const element = screen.getByTestId('clickable')
await user.click(element)
expect(onClick).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,77 @@
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import Slider from './index'
describe('Slider Component', () => {
it('should render with correct default ARIA limits and current value', () => {
render(<Slider value={50} onChange={vi.fn()} />)
const slider = screen.getByRole('slider')
expect(slider).toHaveAttribute('aria-valuemin', '0')
expect(slider).toHaveAttribute('aria-valuemax', '100')
expect(slider).toHaveAttribute('aria-valuenow', '50')
})
it('should apply custom min, max, and step values', () => {
render(<Slider value={10} min={5} max={20} step={5} onChange={vi.fn()} />)
const slider = screen.getByRole('slider')
expect(slider).toHaveAttribute('aria-valuemin', '5')
expect(slider).toHaveAttribute('aria-valuemax', '20')
expect(slider).toHaveAttribute('aria-valuenow', '10')
})
it('should default to 0 if the value prop is NaN', () => {
render(<Slider value={Number.NaN} onChange={vi.fn()} />)
const slider = screen.getByRole('slider')
expect(slider).toHaveAttribute('aria-valuenow', '0')
})
it('should call onChange when arrow keys are pressed', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
render(<Slider value={20} onChange={onChange} />)
const slider = screen.getByRole('slider')
await act(async () => {
slider.focus()
await user.keyboard('{ArrowRight}')
})
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenCalledWith(21, 0)
})
it('should not trigger onChange when disabled', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
render(<Slider value={20} onChange={onChange} disabled />)
const slider = screen.getByRole('slider')
expect(slider).toHaveAttribute('aria-disabled', 'true')
await act(async () => {
slider.focus()
await user.keyboard('{ArrowRight}')
})
expect(onChange).not.toHaveBeenCalled()
})
it('should apply custom class names', () => {
render(
<Slider value={10} onChange={vi.fn()} className="outer-test" thumbClassName="thumb-test" />,
)
const sliderWrapper = screen.getByRole('slider').closest('.outer-test')
expect(sliderWrapper).toBeInTheDocument()
const thumb = screen.getByRole('slider')
expect(thumb).toHaveClass('thumb-test')
})
})

View File

@ -0,0 +1,141 @@
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { describe, expect, it, vi } from 'vitest'
import Sort from './index'
const mockItems = [
{ value: 'created_at', name: 'Date Created' },
{ value: 'name', name: 'Name' },
{ value: 'status', name: 'Status' },
]
describe('Sort component — real portal integration', () => {
const setup = (props = {}) => {
const onSelect = vi.fn()
const user = userEvent.setup()
const { container, rerender } = render(
<Sort value="created_at" items={mockItems} onSelect={onSelect} order="" {...props} />,
)
// helper: returns a non-null HTMLElement or throws with a clear message
const getTriggerWrapper = (): HTMLElement => {
const labelNode = screen.getByText('appLog.filter.sortBy')
// try to find a reasonable wrapper element; prefer '.block' but fallback to any ancestor div
const wrapper = labelNode.closest('.block') ?? labelNode.closest('div')
if (!wrapper)
throw new Error('Trigger wrapper element not found for "Sort by" label')
return wrapper as HTMLElement
}
// helper: returns right-side sort button element
const getSortButton = (): HTMLElement => {
const btn = container.querySelector('.rounded-r-lg')
if (!btn)
throw new Error('Sort button (rounded-r-lg) not found in rendered container')
return btn as HTMLElement
}
return { user, onSelect, rerender, getTriggerWrapper, getSortButton }
}
it('renders and shows selected item label and sort icon', () => {
const { getSortButton } = setup({ order: '' })
expect(screen.getByText('Date Created')).toBeInTheDocument()
const sortButton = getSortButton()
expect(sortButton).toBeInstanceOf(HTMLElement)
expect(sortButton.querySelector('svg')).toBeInTheDocument()
})
it('opens and closes the tooltip (portal mounts to document.body)', async () => {
const { user, getTriggerWrapper } = setup()
await user.click(getTriggerWrapper())
const tooltip = await screen.findByRole('tooltip')
expect(tooltip).toBeInTheDocument()
expect(document.body.contains(tooltip)).toBe(true)
// clicking the trigger again should close it
await user.click(getTriggerWrapper())
await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
})
it('renders options and calls onSelect with descending prefix when order is "-"', async () => {
const { user, onSelect, getTriggerWrapper } = setup({ order: '-' })
await user.click(getTriggerWrapper())
const tooltip = await screen.findByRole('tooltip')
mockItems.forEach((item) => {
expect(within(tooltip).getByText(item.name)).toBeInTheDocument()
})
await user.click(within(tooltip).getByText('Name'))
expect(onSelect).toHaveBeenCalledWith('-name')
await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
})
it('toggles sorting order: ascending -> descending via right-side button', async () => {
const { user, onSelect, getSortButton } = setup({ order: '', value: 'created_at' })
await user.click(getSortButton())
expect(onSelect).toHaveBeenCalledWith('-created_at')
})
it('toggles sorting order: descending -> ascending via right-side button', async () => {
const { user, onSelect, getSortButton } = setup({ order: '-', value: 'name' })
await user.click(getSortButton())
expect(onSelect).toHaveBeenCalledWith('name')
})
it('shows checkmark only for selected item in menu', async () => {
const { user, getTriggerWrapper } = setup({ value: 'status' })
await user.click(getTriggerWrapper())
const tooltip = await screen.findByRole('tooltip')
const statusRow = within(tooltip).getByText('Status').closest('.flex')
const nameRow = within(tooltip).getByText('Name').closest('.flex')
if (!statusRow)
throw new Error('Status option row not found in menu')
if (!nameRow)
throw new Error('Name option row not found in menu')
expect(statusRow.querySelector('svg')).toBeInTheDocument()
expect(nameRow.querySelector('svg')).not.toBeInTheDocument()
})
it('shows empty selection label when value is unknown', () => {
setup({ value: 'unknown_value' })
const label = screen.getByText('appLog.filter.sortBy')
const valueNode = label.nextSibling
if (!valueNode)
throw new Error('Expected a sibling node for the selection text')
expect(String(valueNode.textContent || '').trim()).toBe('')
})
it('handles undefined order prop without asserting a literal "undefined" prefix', async () => {
const { user, onSelect, getTriggerWrapper } = setup({ order: undefined })
await user.click(getTriggerWrapper())
const tooltip = await screen.findByRole('tooltip')
await user.click(within(tooltip).getByText('Name'))
expect(onSelect).toHaveBeenCalled()
expect(onSelect).toHaveBeenCalledWith(expect.stringMatching(/name$/))
})
it('clicking outside the open menu closes the portal', async () => {
const { user, getTriggerWrapper } = setup()
await user.click(getTriggerWrapper())
const tooltip = await screen.findByRole('tooltip')
expect(tooltip).toBeInTheDocument()
// click outside: body click should close the tooltip
await user.click(document.body)
await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument())
})
})

View File

@ -0,0 +1,84 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import Switch from './index'
describe('Switch', () => {
it('should render in unchecked state by default', () => {
render(<Switch />)
const switchElement = screen.getByRole('switch')
expect(switchElement).toBeInTheDocument()
expect(switchElement).toHaveAttribute('aria-checked', 'false')
})
it('should render in checked state when defaultValue is true', () => {
render(<Switch defaultValue={true} />)
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveAttribute('aria-checked', 'true')
})
it('should toggle state and call onChange when clicked', async () => {
const onChange = vi.fn()
const user = userEvent.setup()
render(<Switch onChange={onChange} />)
const switchElement = screen.getByRole('switch')
await user.click(switchElement)
expect(switchElement).toHaveAttribute('aria-checked', 'true')
expect(onChange).toHaveBeenCalledWith(true)
expect(onChange).toHaveBeenCalledTimes(1)
await user.click(switchElement)
expect(switchElement).toHaveAttribute('aria-checked', 'false')
expect(onChange).toHaveBeenCalledWith(false)
expect(onChange).toHaveBeenCalledTimes(2)
})
it('should not call onChange when disabled', async () => {
const onChange = vi.fn()
const user = userEvent.setup()
render(<Switch disabled onChange={onChange} />)
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('!cursor-not-allowed', '!opacity-50')
await user.click(switchElement)
expect(onChange).not.toHaveBeenCalled()
})
it('should apply correct size classes', () => {
const { rerender } = render(<Switch size="xs" />)
// We only need to find the element once
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('h-2.5', 'w-3.5', 'rounded-sm')
rerender(<Switch size="sm" />)
expect(switchElement).toHaveClass('h-3', 'w-5')
rerender(<Switch size="md" />)
expect(switchElement).toHaveClass('h-4', 'w-7')
rerender(<Switch size="l" />)
expect(switchElement).toHaveClass('h-5', 'w-9')
rerender(<Switch size="lg" />)
expect(switchElement).toHaveClass('h-6', 'w-11')
})
it('should apply custom className', () => {
render(<Switch className="custom-test-class" />)
expect(screen.getByRole('switch')).toHaveClass('custom-test-class')
})
it('should apply correct background colors based on state', async () => {
const user = userEvent.setup()
render(<Switch />)
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveClass('bg-components-toggle-bg-unchecked')
await user.click(switchElement)
expect(switchElement).toHaveClass('bg-components-toggle-bg')
})
})

View File

@ -0,0 +1,104 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import Tag from './index'
import '@testing-library/jest-dom/vitest'
describe('Tag Component', () => {
describe('Rendering', () => {
it('should render with text children', () => {
const { container } = render(<Tag>Hello World</Tag>)
expect(container.firstChild).toHaveTextContent('Hello World')
})
it('should render with ReactNode children', () => {
render(<Tag><span data-testid="child">Node</span></Tag>)
expect(screen.getByTestId('child')).toBeInTheDocument()
})
it('should always apply base layout classes', () => {
const { container } = render(<Tag>Test</Tag>)
expect(container.firstChild).toHaveClass(
'inline-flex',
'shrink-0',
'items-center',
'rounded-md',
'px-2.5',
'py-px',
'text-xs',
'leading-5',
)
})
})
describe('Color Variants', () => {
it.each([
{ color: 'green', text: 'text-green-800', bg: 'bg-green-100' },
{ color: 'yellow', text: 'text-yellow-800', bg: 'bg-yellow-100' },
{ color: 'red', text: 'text-red-800', bg: 'bg-red-100' },
{ color: 'gray', text: 'text-gray-800', bg: 'bg-gray-100' },
])('should apply $color color classes', ({ color, text, bg }) => {
type colorType = 'green' | 'yellow' | 'red' | 'gray' | undefined
const { container } = render(<Tag color={color as colorType}>Test</Tag>)
expect(container.firstChild).toHaveClass(text, bg)
})
it('should default to green when no color specified', () => {
const { container } = render(<Tag>Test</Tag>)
expect(container.firstChild).toHaveClass('text-green-800', 'bg-green-100')
})
it('should not apply color classes for invalid color', () => {
type colorType = 'green' | 'yellow' | 'red' | 'gray' | undefined
const { container } = render(<Tag color={'invalid' as colorType}>Test</Tag>)
const className = (container.firstChild as HTMLElement)?.className || ''
expect(className).not.toMatch(/text-(green|yellow|red|gray)-800/)
expect(className).not.toMatch(/bg-(green|yellow|red|gray)-100/)
})
})
describe('Boolean Props', () => {
it('should apply border when bordered is true', () => {
const { container } = render(<Tag bordered>Test</Tag>)
expect(container.firstChild).toHaveClass('border-[1px]')
})
it('should not apply border by default', () => {
const { container } = render(<Tag>Test</Tag>)
expect(container.firstChild).not.toHaveClass('border-[1px]')
})
it('should hide background when hideBg is true', () => {
const { container } = render(<Tag hideBg>Test</Tag>)
expect(container.firstChild).toHaveClass('bg-transparent')
})
it('should apply both bordered and hideBg together', () => {
const { container } = render(<Tag bordered hideBg>Test</Tag>)
expect(container.firstChild).toHaveClass('border-[1px]', 'bg-transparent')
})
it('should override color background with hideBg', () => {
const { container } = render(<Tag color="red" hideBg>Test</Tag>)
const tag = container.firstChild
expect(tag).toHaveClass('bg-transparent', 'text-red-800')
})
})
describe('Custom Styling', () => {
it('should merge custom className', () => {
const { container } = render(<Tag className="my-custom-class">Test</Tag>)
expect(container.firstChild).toHaveClass('my-custom-class')
})
it('should preserve base classes with custom className', () => {
const { container } = render(<Tag className="my-custom-class">Test</Tag>)
expect(container.firstChild).toHaveClass('inline-flex', 'my-custom-class')
})
it('should handle empty className prop', () => {
const { container } = render(<Tag className="">Test</Tag>)
expect(container.firstChild).toBeInTheDocument()
})
})
})