import { describe, expect, it, vi, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import PersonMultiSelect from './PersonMultiSelect.svelte'; const waitForDebounce = () => new Promise((r) => setTimeout(r, 350)); const tick = () => new Promise((r) => setTimeout(r, 0)); const PERSONS = [ { id: '1', firstName: 'Max', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON' }, { id: '2', firstName: 'Anna', lastName: 'Musterfrau', displayName: 'Anna Musterfrau', personType: 'PERSON' }, { id: '3', firstName: 'Karl', lastName: 'König', displayName: 'Karl König', personType: 'PERSON' } ]; function mockFetch(persons = PERSONS) { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue(persons) }) ); } function receiverInputs() { return Array.from( document.querySelectorAll('input[type="hidden"][name="receiverIds"]') ); } afterEach(() => { cleanup(); vi.unstubAllGlobals(); }); // ─── Rendering ──────────────────────────────────────────────────────────────── describe('PersonMultiSelect – rendering', () => { it('renders the text input with placeholder when no persons selected', async () => { render(PersonMultiSelect, { selectedPersons: [] }); await expect.element(page.getByPlaceholder('Namen tippen...')).toBeInTheDocument(); await page.screenshot({ path: 'test-results/screenshots/person-multiselect-empty.png' }); }); it('renders pre-selected persons as chips', async () => { render(PersonMultiSelect, { selectedPersons: [ { id: '1', firstName: 'Max', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON' }, { id: '2', firstName: 'Anna', lastName: 'Musterfrau', displayName: 'Anna Musterfrau', personType: 'PERSON' } ] }); await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument(); await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument(); await page.screenshot({ path: 'test-results/screenshots/person-multiselect-with-chips.png' }); }); it('renders hidden inputs for each selected person', async () => { render(PersonMultiSelect, { selectedPersons: [ { id: '1', firstName: 'Max', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON' }, { id: '2', firstName: 'Anna', lastName: 'Musterfrau', displayName: 'Anna Musterfrau', personType: 'PERSON' } ] }); await tick(); const inputs = receiverInputs(); expect(inputs).toHaveLength(2); expect(inputs[0].value).toBe('1'); expect(inputs[1].value).toBe('2'); }); it('hides the placeholder when persons are selected', async () => { render(PersonMultiSelect, { selectedPersons: [ { id: '1', firstName: 'Max', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON' } ] }); await expect.element(page.getByPlaceholder('Namen tippen...')).not.toBeInTheDocument(); }); }); // ─── Selecting persons ──────────────────────────────────────────────────────── describe('PersonMultiSelect – selecting persons', () => { it('adds a person chip on result click', async () => { mockFetch(); render(PersonMultiSelect, { selectedPersons: [] }); const input = page.getByRole('textbox'); await input.fill('Mu'); await waitForDebounce(); await page.getByText('Max Mustermann').click(); await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument(); await expect.element(input).toHaveValue(''); await page.screenshot({ path: 'test-results/screenshots/person-multiselect-one-selected.png' }); }); it('can select multiple persons sequentially', async () => { mockFetch(); render(PersonMultiSelect, { selectedPersons: [] }); const input = page.getByRole('textbox'); await input.fill('Mu'); await waitForDebounce(); await page.getByText('Max Mustermann').click(); await input.fill('Mu'); await waitForDebounce(); await page.getByText('Anna Musterfrau').click(); await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument(); await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument(); await page.screenshot({ path: 'test-results/screenshots/person-multiselect-two-selected.png' }); }); it('filters already-selected persons from search results', async () => { mockFetch(); render(PersonMultiSelect, { selectedPersons: [ { id: '1', firstName: 'Max', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON' } ] }); const input = page.getByRole('textbox'); await input.fill('Mu'); await waitForDebounce(); // Chip still shows "Max Mustermann" but the dropdown item (role=button) must be filtered out await expect .element(page.getByRole('button', { name: 'Max Mustermann' })) .not.toBeInTheDocument(); await expect.element(page.getByRole('button', { name: 'Anna Musterfrau' })).toBeInTheDocument(); }); it('selects a result with Enter key', async () => { mockFetch([ { id: '1', firstName: 'Max', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON' } ]); render(PersonMultiSelect, { selectedPersons: [] }); const input = page.getByRole('textbox'); await input.fill('Ma'); await waitForDebounce(); await page.getByText('Max Mustermann').click(); await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument(); }); }); // ─── Removing persons ───────────────────────────────────────────────────────── describe('PersonMultiSelect – removing persons', () => { it('removes a chip when its × button is clicked', async () => { render(PersonMultiSelect, { selectedPersons: [ { id: '1', firstName: 'Max', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON' }, { id: '2', firstName: 'Anna', lastName: 'Musterfrau', displayName: 'Anna Musterfrau', personType: 'PERSON' } ] }); // Buttons have aria-label="Entfernen" const removeButtons = page.getByRole('button', { name: 'Entfernen' }); await removeButtons.first().click(); await expect.element(page.getByText('Max Mustermann')).not.toBeInTheDocument(); await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument(); }); it('removes the corresponding hidden input when a chip is removed', async () => { render(PersonMultiSelect, { selectedPersons: [ { id: '1', firstName: 'Max', lastName: 'Mustermann', displayName: 'Max Mustermann', personType: 'PERSON' }, { id: '2', firstName: 'Anna', lastName: 'Musterfrau', displayName: 'Anna Musterfrau', personType: 'PERSON' } ] }); await page.getByRole('button', { name: 'Entfernen' }).first().click(); await tick(); const inputs = receiverInputs(); expect(inputs).toHaveLength(1); expect(inputs[0].value).toBe('2'); }); }); // ─── Click outside ──────────────────────────────────────────────────────────── describe('PersonMultiSelect – click outside', () => { it('closes the dropdown when clicking outside', async () => { mockFetch(); render(PersonMultiSelect, { selectedPersons: [] }); const input = page.getByRole('textbox'); await input.fill('Mu'); await waitForDebounce(); await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument(); document.body.click(); await tick(); await expect.element(page.getByText('Max Mustermann')).not.toBeInTheDocument(); }); });