- role="tooltip" → role="region" + aria-label={label}: tooltip semantics
are wrong for a click-triggered panel (Nora/Sara)
- expand button to 44×44px with inner visual <span>: WCAG 2.5.8 touch
target for 60+ transcriber audience (Sara/Leonie)
- replace Math.random() with module-level counter: SSR/hydration mismatch
when server and client generate different IDs (Felix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
3.6 KiB
TypeScript
91 lines
3.6 KiB
TypeScript
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+$/);
|
|
});
|
|
});
|