import * as React from 'react' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, expect, it, vi } from 'vitest' import { axe } from 'vitest-axe' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './accordion' function Basic({ defaultValue, disabled, multiple, itemDisabled, }: { defaultValue?: string[] disabled?: boolean multiple?: boolean itemDisabled?: boolean } = {}) { return ( First

First body

Second

Second body

) } describe('Accordion', () => { it('renders trigger with data-slot', () => { render() const trigger = screen.getByRole('button', { name: 'First' }) expect(trigger).toHaveAttribute('data-slot', 'accordion-trigger') }) it('defaults closed: panels not in the DOM', () => { render() expect(screen.queryByText('First body')).toBeNull() expect(screen.queryByText('Second body')).toBeNull() }) it('opens on trigger click and closes on a second click', async () => { const user = userEvent.setup() render() const trigger = screen.getByRole('button', { name: 'First' }) await user.click(trigger) const panel = await screen.findByText('First body') expect(panel.closest('[data-slot="accordion-content"]')).not.toBeNull() expect(trigger).toHaveAttribute('data-panel-open') await user.click(trigger) await waitFor(() => { expect(screen.queryByText('First body')).toBeNull() }) }) it('respects defaultValue', () => { render() expect(screen.getByText('Second body')).toBeInTheDocument() expect(screen.queryByText('First body')).toBeNull() }) it('disabled root prevents toggling and reflects data-disabled', async () => { const user = userEvent.setup() render() const trigger = screen.getByRole('button', { name: 'First' }) expect(trigger.getAttribute('data-disabled')).not.toBeNull() await user.click(trigger) expect(screen.queryByText('First body')).toBeNull() }) it('disabled item prevents toggling for that item only', async () => { const user = userEvent.setup() render() const first = screen.getByRole('button', { name: 'First' }) const second = screen.getByRole('button', { name: 'Second' }) expect(first.getAttribute('data-disabled')).not.toBeNull() await user.click(first) expect(screen.queryByText('First body')).toBeNull() await user.click(second) await screen.findByText('Second body') }) it('single mode: opening a second item closes the first', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('button', { name: 'First' })) await screen.findByText('First body') await user.click(screen.getByRole('button', { name: 'Second' })) await screen.findByText('Second body') await waitFor(() => { expect(screen.queryByText('First body')).toBeNull() }) }) it('multiple mode: both panels can be open at once', async () => { const user = userEvent.setup() render() await user.click(screen.getByRole('button', { name: 'First' })) await user.click(screen.getByRole('button', { name: 'Second' })) await screen.findByText('First body') await screen.findByText('Second body') }) it('controlled mode: onValueChange fires with the next value array', async () => { const user = userEvent.setup() const onValueChange = vi.fn() function Controlled() { const [value, setValue] = React.useState([]) return ( { onValueChange(next, details) setValue(next as string[]) }} > Toggle

Panel body

) } render() await user.click(screen.getByRole('button', { name: 'Toggle' })) expect(onValueChange).toHaveBeenCalledTimes(1) expect(onValueChange).toHaveBeenLastCalledWith(['one'], expect.anything()) await screen.findByText('Panel body') }) it('forwards className on each part', async () => { const user = userEvent.setup() render( Toggle

Panel body

, ) const root = screen.getByRole('button', { name: 'Toggle' }).closest('[data-slot="accordion"]') as HTMLElement expect(root.className).toContain('root-cls') const item = screen.getByRole('button', { name: 'Toggle' }).closest('[data-slot="accordion-item"]') as HTMLElement expect(item.className).toContain('item-cls') const trigger = screen.getByRole('button', { name: 'Toggle' }) expect(trigger.className).toContain('trigger-cls') expect(trigger.className).toContain('cursor-pointer') await user.click(trigger) const panel = (await screen.findByText('Panel body')).closest('[data-slot="accordion-content"]') as HTMLElement expect(panel.className).toContain('content-cls') expect(panel.className).toContain('overflow-hidden') }) it('icon={false} renders no icon SVG', () => { render( No icon body , ) const trigger = screen.getByRole('button', { name: 'No icon' }) expect(trigger.querySelector('[data-slot="accordion-icon"]')).toBeNull() }) it('iconVariant="icon-box" wraps the icon in a styled box', () => { render( Boxed body , ) const trigger = screen.getByRole('button', { name: 'Boxed' }) const box = trigger.querySelector('[data-slot="accordion-trigger-icon-box"]') expect(box).not.toBeNull() expect(box?.querySelector('[data-slot="accordion-icon"]')).not.toBeNull() }) it('iconPosition="start" renders the icon before the children', () => { render( Label body , ) const trigger = screen.getByRole('button', { name: 'Label' }) const firstChild = trigger.firstElementChild expect(firstChild?.querySelector('[data-slot="accordion-icon"]')).not.toBeNull() }) it('has no a11y violations when open', async () => { const { container } = render() expect(await axe(container)).toHaveNoViolations() }) })