Add displayName and personType to all Person mock objects in component and page tests. Update assertions from reversed "lastName, firstName" format to forward-order displayName. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
273 lines
8.0 KiB
TypeScript
273 lines
8.0 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',
|
||
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<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',
|
||
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();
|
||
});
|
||
});
|