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 { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from './popover' function renderPopover({ side, title = 'Notifications', description = 'You are all caught up.', }: { side?: 'top' | 'bottom' | 'left' | 'right' title?: string description?: string } = {}) { return render( Open} /> {title} {description} Dismiss} /> , ) } describe('Popover', () => { it('renders the trigger and tags it with data-slot', () => { renderPopover() const trigger = screen.getByRole('button', { name: 'Open' }) expect(trigger.getAttribute('data-slot')).toBe('popover-trigger') }) it('does not render content before being opened', () => { renderPopover({ description: 'Hidden until click' }) expect(screen.queryByText('Hidden until click')).toBeNull() }) it('keeps the content mounted while closed when keepMounted is set', () => { render( Open} /> Always in the DOM , ) // Never opened, yet present in the DOM (hidden) so a consumer's animated // content doesn't re-mount - and re-animate - on every open. expect(document.querySelector('[data-slot="popover-content"]')).not.toBeNull() expect(screen.getByText('Always in the DOM')).toBeInTheDocument() }) it('opens on click and closes on outside click', async () => { const user = userEvent.setup() renderPopover() await user.click(screen.getByRole('button', { name: 'Open' })) const description = await screen.findByText('You are all caught up.') expect(description).toBeTruthy() const popup = description.closest('[data-slot="popover-content"]') as HTMLElement expect(popup).not.toBeNull() expect(popup.className).toContain('bg-background') expect(popup.className).toContain('rounded-xl') expect(popup.className).toContain('border-border-overlay') await user.click(document.body) await waitFor(() => { expect(screen.queryByText('You are all caught up.')).toBeNull() }) }) it('closes when PopoverClose is clicked', async () => { const user = userEvent.setup() renderPopover() await user.click(screen.getByRole('button', { name: 'Open' })) await screen.findByText('You are all caught up.') await user.click(screen.getByRole('button', { name: 'Dismiss' })) await waitFor(() => { expect(screen.queryByText('You are all caught up.')).toBeNull() }) }) it('renders the title with intense foreground styles', async () => { const user = userEvent.setup() renderPopover({ title: 'Heading' }) await user.click(screen.getByRole('button', { name: 'Open' })) const title = await screen.findByText('Heading') expect(title.getAttribute('data-slot')).toBe('popover-title') expect(title.className).toContain('text-foreground-intense') expect(title.className).toContain('font-semibold') }) it('renders the arrow svg inside the popup', async () => { const user = userEvent.setup() renderPopover() await user.click(screen.getByRole('button', { name: 'Open' })) await screen.findByText('You are all caught up.') const arrow = document.querySelector('[data-slot="popover-arrow"]') as HTMLElement | null expect(arrow).not.toBeNull() expect(arrow!.querySelector('svg')).not.toBeNull() }) it('omits the arrow and the thicker side border when arrow is false', async () => { const user = userEvent.setup() render( Open} /> No arrow here. , ) await user.click(screen.getByRole('button', { name: 'Open' })) const description = await screen.findByText('No arrow here.') expect(document.querySelector('[data-slot="popover-arrow"]')).toBeNull() const popup = description.closest('[data-slot="popover-content"]') as HTMLElement expect(popup.className).not.toMatch(/border-[a-z]-2/) }) it('has no accessibility violations (closed state)', async () => { const { container } = renderPopover() expect(await axe(container)).toHaveNoViolations() }) })