Files
familienarchiv/frontend/src/routes/search/DisambiguationPicker.svelte.spec.ts
Marcel 8429b1e9f8 fix(search): derive disambiguation trigger aria-label from match count (#763 review)
The trigger hardcoded the multiple-people label for every count, so a
single did-you-mean picker announced "Mehrere Personen gefunden" to
screen readers while sighted users saw one name and a "Meintest du …?"
heading. Derive the trigger's accessible name from persons.length: a
single suggestion reuses the heading prop, two or more keep the
multiple-people label. Visible truncated name span unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:47:47 +02:00

119 lines
4.4 KiB
TypeScript

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' }
];
const multiProps = { persons, heading: 'Person auswählen', showCue: true };
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, { ...multiProps, 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, { ...multiProps, 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, { ...multiProps, 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, { ...multiProps, 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, { ...multiProps, 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]);
});
it('renders the supplied heading as a visible panel heading', async () => {
render(DisambiguationPicker, {
persons: [{ id: 'c1', displayName: 'Clara Cramer' }],
heading: 'Meintest du Clara Cramer?',
showCue: false,
onSelect: vi.fn()
});
await page.getByRole('button', { name: 'Meintest du Clara Cramer?' }).click();
await expect.element(page.getByText('Meintest du Clara Cramer?')).toBeVisible();
});
it('suppresses the cue when showCue is false', async () => {
render(DisambiguationPicker, {
persons: [{ id: 'c1', displayName: 'Clara Cramer' }],
heading: 'Meintest du Clara Cramer?',
showCue: false,
onSelect: vi.fn()
});
await expect.element(page.getByText('(auswählen…)')).not.toBeInTheDocument();
});
it('shows the cue when showCue is true', async () => {
render(DisambiguationPicker, { ...multiProps, onSelect: vi.fn() });
await expect.element(page.getByText('(auswählen…)')).toBeVisible();
});
it('announces the did-you-mean heading as the trigger accessible name for a single suggestion', async () => {
render(DisambiguationPicker, {
persons: [{ id: 'c1', displayName: 'Clara Cramer' }],
heading: 'Meintest du Clara Cramer?',
showCue: false,
onSelect: vi.fn()
});
await expect
.element(page.getByRole('button', { name: 'Meintest du Clara Cramer?' }))
.toBeInTheDocument();
});
it('keeps the multiple-people trigger accessible name for two or more suggestions', async () => {
render(DisambiguationPicker, { ...multiProps, onSelect: vi.fn() });
await expect
.element(page.getByRole('button', { name: /Mehrere Personen gefunden/ }))
.toBeInTheDocument();
});
});