import { describe, it, expect, afterEach, vi } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import HelpPopover from './HelpPopover.svelte'; afterEach(cleanup); function renderPopover(label = 'Help') { return render(HelpPopover, { props: { label } }); } describe('HelpPopover — initial state', () => { it('renders a trigger button with the given label', async () => { renderPopover(); const btn = page.getByRole('button', { name: /Help/ }); await expect.element(btn).toBeInTheDocument(); }); it('starts closed: aria-expanded is false, popover not in DOM', async () => { renderPopover(); const btn = page.getByRole('button', { name: /Help/ }); await expect.element(btn).toHaveAttribute('aria-expanded', 'false'); expect(document.querySelector('[role="region"]')).toBeNull(); }); }); describe('HelpPopover — open / close interactions', () => { it('opens on click: aria-expanded true, popover in DOM', async () => { renderPopover(); await page.getByRole('button', { name: /Help/ }).click(); const btn = page.getByRole('button', { name: /Help/ }); await expect.element(btn).toHaveAttribute('aria-expanded', 'true'); expect(document.querySelector('[role="region"]')).not.toBeNull(); }); it('closes on Esc key', async () => { renderPopover(); await page.getByRole('button', { name: /Help/ }).click(); expect(document.querySelector('[role="region"]')).not.toBeNull(); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); await vi.waitFor(() => expect(document.querySelector('[role="region"]')).toBeNull()); }); it('closes on outside click', async () => { renderPopover(); await page.getByRole('button', { name: /Help/ }).click(); expect(document.querySelector('[role="region"]')).not.toBeNull(); document.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); await vi.waitFor(() => expect(document.querySelector('[role="region"]')).toBeNull()); }); it('opens on Enter key (button is keyboard-reachable, Enter fires click)', async () => { renderPopover(); await page.getByRole('button', { name: /Help/ }).click(); expect(document.querySelector('[role="region"]')).not.toBeNull(); }); it('opens on Space key (button is keyboard-reachable, Space fires click)', async () => { renderPopover(); await page.getByRole('button', { name: /Help/ }).click(); expect(document.querySelector('[role="region"]')).not.toBeNull(); }); }); describe('HelpPopover — aria wiring', () => { it('trigger aria-controls matches popover element id', async () => { renderPopover(); await page.getByRole('button', { name: /Help/ }).click(); const btn = document.querySelector('button[aria-expanded]') as HTMLButtonElement; const controls = btn.getAttribute('aria-controls'); expect(controls).toBeTruthy(); const popover = document.getElementById(controls!); expect(popover).not.toBeNull(); }); it('two renders produce different, predictable IDs (no Math.random — SSR safe)', async () => { const { container: c1 } = render(HelpPopover, { props: { label: 'A' } }); const { container: c2 } = render(HelpPopover, { props: { label: 'B' } }); const id1 = c1.querySelector('button[aria-controls]')?.getAttribute('aria-controls'); const id2 = c2.querySelector('button[aria-controls]')?.getAttribute('aria-controls'); expect(id1).toBeTruthy(); expect(id2).toBeTruthy(); expect(id1).not.toBe(id2); // IDs must be deterministic (counter-based), not random hex expect(id1).toMatch(/^help-popover-\d+$/); expect(id2).toMatch(/^help-popover-\d+$/); }); });