feat(search): add DisambiguationPicker single-select disclosure (#739)
Accessible disclosure: aria-expanded/aria-controls trigger, focus moves into the option list on open, Escape and click-outside close and return focus to the trigger, selecting a candidate emits onSelect. Single-select (GET re-run) per the resolved #738 open decision — backend has no multi-sender OR param. 5 vitest-browser-svelte specs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it, vi, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import DisambiguationPicker from './DisambiguationPicker.svelte';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type PersonHint = components['schemas']['PersonHint'];
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
const persons: PersonHint[] = [
|
||||
{ id: 'w1', displayName: 'Walter Raddatz' },
|
||||
{ id: 'w2', displayName: 'Walter Müller' }
|
||||
];
|
||||
|
||||
function pressEscape() {
|
||||
(document.activeElement as HTMLElement).dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
|
||||
);
|
||||
}
|
||||
|
||||
describe('DisambiguationPicker', () => {
|
||||
it('opens the picker and shows a select option per ambiguous person', async () => {
|
||||
render(DisambiguationPicker, { persons, onSelect: vi.fn() });
|
||||
await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click();
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Walter Raddatz auswählen' }))
|
||||
.toBeInTheDocument();
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Walter Müller auswählen' }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('moves focus into the picker list on open', async () => {
|
||||
render(DisambiguationPicker, { persons, onSelect: vi.fn() });
|
||||
await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click();
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Walter Raddatz auswählen' }))
|
||||
.toHaveFocus();
|
||||
});
|
||||
|
||||
it('returns focus to the trigger when closed with Escape', async () => {
|
||||
render(DisambiguationPicker, { persons, onSelect: vi.fn() });
|
||||
const trigger = page.getByRole('button', { name: /Mehrere Personen gefunden/ });
|
||||
await trigger.click();
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Walter Raddatz auswählen' }))
|
||||
.toHaveFocus();
|
||||
pressEscape();
|
||||
await expect.element(trigger).toHaveFocus();
|
||||
});
|
||||
|
||||
it('does not call onSelect when dismissed without choosing', async () => {
|
||||
const onSelect = vi.fn();
|
||||
render(DisambiguationPicker, { persons, onSelect });
|
||||
await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click();
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Walter Raddatz auswählen' }))
|
||||
.toHaveFocus();
|
||||
pressEscape();
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onSelect with the chosen person', async () => {
|
||||
const onSelect = vi.fn();
|
||||
render(DisambiguationPicker, { persons, onSelect });
|
||||
await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click();
|
||||
await page.getByRole('button', { name: 'Walter Müller auswählen' }).click();
|
||||
expect(onSelect).toHaveBeenCalledWith(persons[1]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user