From 7c3a8e76519d77f161f6159b1c6c8c27069a94f6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 24 Apr 2026 21:19:48 +0200 Subject: [PATCH] feat(transcribe): add HelpPopover primitive and wire (?) chip into panel header Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/HelpPopover.svelte | 84 +++++++++++++++++++ .../lib/components/HelpPopover.svelte.spec.ts | 77 +++++++++++++++++ .../TranscriptionPanelHeader.svelte | 48 ++++++----- .../TranscriptionPanelHeader.svelte.test.ts | 35 +++++++- 4 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 frontend/src/lib/components/HelpPopover.svelte create mode 100644 frontend/src/lib/components/HelpPopover.svelte.spec.ts diff --git a/frontend/src/lib/components/HelpPopover.svelte b/frontend/src/lib/components/HelpPopover.svelte new file mode 100644 index 00000000..e1d118ca --- /dev/null +++ b/frontend/src/lib/components/HelpPopover.svelte @@ -0,0 +1,84 @@ + + +
+ + + {#if open} + + {/if} +
diff --git a/frontend/src/lib/components/HelpPopover.svelte.spec.ts b/frontend/src/lib/components/HelpPopover.svelte.spec.ts new file mode 100644 index 00000000..b2e5ae16 --- /dev/null +++ b/frontend/src/lib/components/HelpPopover.svelte.spec.ts @@ -0,0 +1,77 @@ +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="tooltip"]')).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="tooltip"]')).not.toBeNull(); + }); + + it('closes on Esc key', async () => { + renderPopover(); + await page.getByRole('button', { name: /Help/ }).click(); + expect(document.querySelector('[role="tooltip"]')).not.toBeNull(); + + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); + await vi.waitFor(() => expect(document.querySelector('[role="tooltip"]')).toBeNull()); + }); + + it('closes on outside click', async () => { + renderPopover(); + await page.getByRole('button', { name: /Help/ }).click(); + expect(document.querySelector('[role="tooltip"]')).not.toBeNull(); + + document.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); + await vi.waitFor(() => expect(document.querySelector('[role="tooltip"]')).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="tooltip"]')).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="tooltip"]')).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(); + }); +}); diff --git a/frontend/src/lib/components/TranscriptionPanelHeader.svelte b/frontend/src/lib/components/TranscriptionPanelHeader.svelte index c3ceeb69..6bf40a5d 100644 --- a/frontend/src/lib/components/TranscriptionPanelHeader.svelte +++ b/frontend/src/lib/components/TranscriptionPanelHeader.svelte @@ -1,6 +1,7 @@