role=listbox + role=option without arrow-key navigation is misleading — the
WAI-ARIA combobox pattern requires aria-activedescendant handling that isn't
implemented. Downgraded to plain <ul>/<li>; input keeps role=combobox +
aria-controls pointing to the list id.
listboxId was a module-level constant so two simultaneous instances would share
the same DOM id. Fixed with a <script module> counter.
Updated spec queries from getByRole('option') to getByText() — tests behaviour,
not the ARIA implementation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
2.8 KiB
TypeScript
96 lines
2.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
import { page, userEvent } from 'vitest/browser';
|
|
import DocumentPickerDropdown from './DocumentPickerDropdown.svelte';
|
|
|
|
const waitForDebounce = () => new Promise((r) => setTimeout(r, 350));
|
|
|
|
const docFactory = (id: string, title: string) => ({
|
|
id,
|
|
title,
|
|
documentDate: '1880-01-01',
|
|
metaDatePrecision: 'DAY' as const,
|
|
metaDateEnd: undefined
|
|
});
|
|
|
|
function mockSearchResponse(items: ReturnType<typeof docFactory>[]) {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: vi.fn().mockResolvedValue({ items })
|
|
})
|
|
);
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('DocumentPickerDropdown — empty query guard', () => {
|
|
it('does not call fetch on empty query', async () => {
|
|
const fetchMock = vi.fn();
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
render(DocumentPickerDropdown, { onSelect: vi.fn() });
|
|
await userEvent.fill(page.getByRole('combobox'), '');
|
|
await waitForDebounce();
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('DocumentPickerDropdown — already-added indicator', () => {
|
|
it('shows already-added document as aria-disabled with sr-only hint', async () => {
|
|
mockSearchResponse([docFactory('d1', 'Brief von Eugenie'), docFactory('d2', 'Brief 2')]);
|
|
|
|
render(DocumentPickerDropdown, {
|
|
alreadyAddedIds: new Set(['d1']),
|
|
onSelect: vi.fn()
|
|
});
|
|
|
|
await userEvent.fill(page.getByRole('combobox'), 'Brief');
|
|
await waitForDebounce();
|
|
|
|
const disabledItem = page
|
|
.getByText(/Brief von Eugenie/i)
|
|
.element()
|
|
.closest('li')!;
|
|
expect(disabledItem.getAttribute('aria-disabled')).toBe('true');
|
|
// Screen-reader text "bereits enthalten" must be present in the item
|
|
await expect.element(page.getByText(/bereits enthalten/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('DocumentPickerDropdown — selection', () => {
|
|
it('calls onSelect with the item when a non-disabled option is clicked', async () => {
|
|
const onSelect = vi.fn();
|
|
mockSearchResponse([docFactory('d1', 'Brief von Eugenie')]);
|
|
|
|
render(DocumentPickerDropdown, { onSelect });
|
|
|
|
await userEvent.fill(page.getByRole('combobox'), 'Brief');
|
|
await waitForDebounce();
|
|
await userEvent.click(page.getByText(/Brief von Eugenie/i));
|
|
|
|
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'd1' }));
|
|
});
|
|
|
|
it('does not call onSelect when an aria-disabled option is clicked', async () => {
|
|
const onSelect = vi.fn();
|
|
mockSearchResponse([docFactory('d1', 'Brief von Eugenie')]);
|
|
|
|
render(DocumentPickerDropdown, {
|
|
alreadyAddedIds: new Set(['d1']),
|
|
onSelect
|
|
});
|
|
|
|
await userEvent.fill(page.getByRole('combobox'), 'Brief');
|
|
await waitForDebounce();
|
|
await page.getByText(/Brief von Eugenie/i).click({ force: true });
|
|
|
|
expect(onSelect).not.toHaveBeenCalled();
|
|
});
|
|
});
|