import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it } from 'vitest'
import { axe } from 'vitest-axe'
import { Button } from '../button/button'
import {
Drawer,
DrawerBody,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from './drawer'
function renderDrawer({
side,
title = 'Edit profile',
description = 'Update your details below.',
closeButton,
backdrop,
frame,
contentClassName,
}: {
side?: 'top' | 'bottom' | 'left' | 'right'
title?: string
description?: string
closeButton?: boolean
backdrop?: boolean
frame?: boolean
contentClassName?: string
} = {}) {
return render(
Open drawer} />
{title}
{description}
Body content
Cancel} />
,
)
}
describe('Drawer', () => {
it('tags the trigger with data-slot', () => {
renderDrawer()
expect(screen.getByRole('button', { name: 'Open drawer' }).getAttribute('data-slot')).toBe('drawer-trigger')
})
it('does not render content before being opened', () => {
renderDrawer()
expect(screen.queryByText('Body content')).toBeNull()
})
it('opens on trigger click and exposes a labeled dialog', async () => {
const user = userEvent.setup()
renderDrawer()
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
const dialog = await screen.findByRole('dialog')
expect(dialog).toHaveAccessibleName('Edit profile')
expect(dialog).toHaveAccessibleDescription('Update your details below.')
const title = screen.getByText('Edit profile')
expect(title.getAttribute('data-slot')).toBe('drawer-title')
expect(title.className).toContain('text-foreground-intense')
})
it('forwards className to the content popup', async () => {
const user = userEvent.setup()
renderDrawer({ contentClassName: 'w-200' })
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
const dialog = await screen.findByRole('dialog')
expect(dialog.getAttribute('data-slot')).toBe('drawer-popup')
expect(dialog.className).toContain('w-200')
})
it('closes when the built-in close button is clicked', async () => {
const user = userEvent.setup()
renderDrawer()
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
await screen.findByRole('dialog')
await user.click(screen.getByRole('button', { name: 'Close' }))
await waitFor(() => {
expect(screen.queryByRole('dialog')).toBeNull()
})
})
it('closes when a DrawerClose action is clicked', async () => {
const user = userEvent.setup()
renderDrawer()
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
await screen.findByRole('dialog')
await user.click(screen.getByRole('button', { name: 'Cancel' }))
await waitFor(() => {
expect(screen.queryByRole('dialog')).toBeNull()
})
})
it('hides the built-in close button when closeButton is false', async () => {
const user = userEvent.setup()
renderDrawer({ closeButton: false })
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
await screen.findByRole('dialog')
expect(screen.queryByRole('button', { name: 'Close' })).toBeNull()
})
it('omits the backdrop when backdrop is false', async () => {
const user = userEvent.setup()
renderDrawer({ backdrop: false })
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
await screen.findByRole('dialog')
expect(document.querySelector('[data-slot="drawer-backdrop"]')).toBeNull()
})
it('renders a backdrop only for the outermost drawer when nested', async () => {
const user = userEvent.setup()
render(
Open outer} />
Outer
Open nested} />
Nested
,
)
await user.click(screen.getByRole('button', { name: 'Open outer' }))
await screen.findByText('Outer')
await user.click(screen.getByRole('button', { name: 'Open nested' }))
await screen.findByText('Nested')
// Outermost drawer keeps its backdrop; the nested one auto-omits it.
expect(document.querySelectorAll('[data-slot="drawer-backdrop"]')).toHaveLength(1)
// The frame needs a backdrop, so only the outermost drawer is framed.
expect(document.querySelectorAll('[data-slot="drawer-popup"][data-frame]')).toHaveLength(1)
})
it('frames the popup by default and drops the frame when frame is false', async () => {
const user = userEvent.setup()
const { unmount } = renderDrawer()
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
await screen.findByRole('dialog')
expect(document.querySelector('[data-slot="drawer-popup"]')).toHaveAttribute('data-frame')
unmount()
renderDrawer({ frame: false })
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
await screen.findByRole('dialog')
expect(document.querySelector('[data-slot="drawer-popup"]')).not.toHaveAttribute('data-frame')
})
it('drops the frame when the backdrop is off, even if frame is set', async () => {
const user = userEvent.setup()
renderDrawer({ backdrop: false, frame: true })
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
await screen.findByRole('dialog')
expect(document.querySelector('[data-slot="drawer-popup"]')).not.toHaveAttribute('data-frame')
})
it('positions on the requested side', async () => {
const user = userEvent.setup()
renderDrawer({ side: 'right' })
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
const dialog = await screen.findByRole('dialog')
// right-side drawer is full-height and slides in from the inline-end edge
expect(dialog.className).toContain('h-full')
expect(dialog.className).toContain('ps-4')
})
it('has no accessibility violations when open', async () => {
const user = userEvent.setup()
renderDrawer()
await user.click(screen.getByRole('button', { name: 'Open drawer' }))
const dialog = await screen.findByRole('dialog')
expect(await axe(dialog)).toHaveNoViolations()
})
})