Replace Playwright locator .click() calls with native DOM element.click() for all tests that trigger Svelte 5 delegated onclick handlers ($.delegated). Playwright's CDP-based synthetic events don't propagate through Svelte 5's document-level handle_event_propagation delegation mechanism, while native DOM .click() does. Also replace locator.click() with element.focus() for onfocus handler tests, and add cleanup() to afterEach in all spec files missing it to prevent test pollution between runs. Fix TagInput.svelte to use untrack() when reading bindable state after an await to avoid track_reactivity_loss errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
186 lines
6.8 KiB
TypeScript
186 lines
6.8 KiB
TypeScript
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' },
|
||
{ id: '2', firstName: 'Anna', lastName: 'Musterfrau' },
|
||
{ id: '3', firstName: 'Karl', lastName: 'König' }
|
||
];
|
||
|
||
function mockFetch(persons = PERSONS) {
|
||
vi.stubGlobal(
|
||
'fetch',
|
||
vi.fn().mockResolvedValue({
|
||
ok: true,
|
||
json: vi.fn().mockResolvedValue(persons)
|
||
})
|
||
);
|
||
}
|
||
|
||
function receiverInputs() {
|
||
return Array.from(
|
||
document.querySelectorAll<HTMLInputElement>('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' },
|
||
{ id: '2', firstName: 'Anna', lastName: 'Musterfrau' }
|
||
]
|
||
});
|
||
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' },
|
||
{ id: '2', firstName: 'Anna', lastName: 'Musterfrau' }
|
||
]
|
||
});
|
||
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' }]
|
||
});
|
||
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('Mustermann, Max').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('Mustermann, Max').click();
|
||
|
||
await input.fill('Mu');
|
||
await waitForDebounce();
|
||
await page.getByText('Musterfrau, Anna').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' }]
|
||
});
|
||
const input = page.getByRole('textbox');
|
||
await input.fill('Mu');
|
||
await waitForDebounce();
|
||
await expect.element(page.getByText('Mustermann, Max')).not.toBeInTheDocument();
|
||
await expect.element(page.getByText('Musterfrau, Anna')).toBeInTheDocument();
|
||
});
|
||
|
||
it('selects a result with Enter key', async () => {
|
||
mockFetch([{ id: '1', firstName: 'Max', lastName: 'Mustermann' }]);
|
||
render(PersonMultiSelect, { selectedPersons: [] });
|
||
const input = page.getByRole('textbox');
|
||
await input.fill('Ma');
|
||
await waitForDebounce();
|
||
await page.getByText('Mustermann, Max').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' },
|
||
{ id: '2', firstName: 'Anna', lastName: 'Musterfrau' }
|
||
]
|
||
});
|
||
// 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' },
|
||
{ id: '2', firstName: 'Anna', lastName: 'Musterfrau' }
|
||
]
|
||
});
|
||
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('Mustermann, Max')).toBeInTheDocument();
|
||
document.body.click();
|
||
await tick();
|
||
await expect.element(page.getByText('Mustermann, Max')).not.toBeInTheDocument();
|
||
});
|
||
});
|