feat: PersonNameParser enhancements and Person model refactor (#209-#213) #215
@@ -5,10 +5,10 @@ import DocumentMetadataDrawer from './DocumentMetadataDrawer.svelte';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
const sender = { id: 's1', firstName: 'Karl', lastName: 'Müller' };
|
||||
const sender = { id: 's1', firstName: 'Karl', lastName: 'Müller', displayName: 'Karl Müller' };
|
||||
const receivers = [
|
||||
{ id: 'r1', firstName: 'Anna', lastName: 'Schmidt' },
|
||||
{ id: 'r2', firstName: 'Hans', lastName: 'Weber' }
|
||||
{ id: 'r1', firstName: 'Anna', lastName: 'Schmidt', displayName: 'Anna Schmidt' },
|
||||
{ id: 'r2', firstName: 'Hans', lastName: 'Weber', displayName: 'Hans Weber' }
|
||||
];
|
||||
const tags = [
|
||||
{ id: 't1', name: 'Familienbrief' },
|
||||
|
||||
@@ -7,9 +7,21 @@ 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' }
|
||||
{
|
||||
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) {
|
||||
@@ -45,8 +57,20 @@ describe('PersonMultiSelect – rendering', () => {
|
||||
it('renders pre-selected persons as chips', async () => {
|
||||
render(PersonMultiSelect, {
|
||||
selectedPersons: [
|
||||
{ id: '1', firstName: 'Max', lastName: 'Mustermann' },
|
||||
{ id: '2', firstName: 'Anna', lastName: 'Musterfrau' }
|
||||
{
|
||||
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();
|
||||
@@ -57,8 +81,20 @@ describe('PersonMultiSelect – rendering', () => {
|
||||
it('renders hidden inputs for each selected person', async () => {
|
||||
render(PersonMultiSelect, {
|
||||
selectedPersons: [
|
||||
{ id: '1', firstName: 'Max', lastName: 'Mustermann' },
|
||||
{ id: '2', firstName: 'Anna', lastName: 'Musterfrau' }
|
||||
{
|
||||
id: '1',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
displayName: 'Max Mustermann',
|
||||
personType: 'PERSON'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
firstName: 'Anna',
|
||||
lastName: 'Musterfrau',
|
||||
displayName: 'Anna Musterfrau',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
]
|
||||
});
|
||||
await tick();
|
||||
@@ -70,7 +106,15 @@ describe('PersonMultiSelect – rendering', () => {
|
||||
|
||||
it('hides the placeholder when persons are selected', async () => {
|
||||
render(PersonMultiSelect, {
|
||||
selectedPersons: [{ id: '1', firstName: 'Max', lastName: 'Mustermann' }]
|
||||
selectedPersons: [
|
||||
{
|
||||
id: '1',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
displayName: 'Max Mustermann',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
]
|
||||
});
|
||||
await expect.element(page.getByPlaceholder('Namen tippen...')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -85,7 +129,7 @@ describe('PersonMultiSelect – selecting persons', () => {
|
||||
const input = page.getByRole('textbox');
|
||||
await input.fill('Mu');
|
||||
await waitForDebounce();
|
||||
await page.getByText('Mustermann, Max').click();
|
||||
await page.getByText('Max Mustermann').click();
|
||||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||||
await expect.element(input).toHaveValue('');
|
||||
await page.screenshot({
|
||||
@@ -100,11 +144,11 @@ describe('PersonMultiSelect – selecting persons', () => {
|
||||
|
||||
await input.fill('Mu');
|
||||
await waitForDebounce();
|
||||
await page.getByText('Mustermann, Max').click();
|
||||
await page.getByText('Max Mustermann').click();
|
||||
|
||||
await input.fill('Mu');
|
||||
await waitForDebounce();
|
||||
await page.getByText('Musterfrau, Anna').click();
|
||||
await page.getByText('Anna Musterfrau').click();
|
||||
|
||||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument();
|
||||
@@ -116,22 +160,41 @@ describe('PersonMultiSelect – selecting persons', () => {
|
||||
it('filters already-selected persons from search results', async () => {
|
||||
mockFetch();
|
||||
render(PersonMultiSelect, {
|
||||
selectedPersons: [{ id: '1', firstName: 'Max', lastName: 'Mustermann' }]
|
||||
selectedPersons: [
|
||||
{
|
||||
id: '1',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
displayName: 'Max Mustermann',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
]
|
||||
});
|
||||
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();
|
||||
// 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' }]);
|
||||
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('Mustermann, Max').click();
|
||||
await page.getByText('Max Mustermann').click();
|
||||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -142,8 +205,20 @@ 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' }
|
||||
{
|
||||
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"
|
||||
@@ -156,8 +231,20 @@ describe('PersonMultiSelect – removing persons', () => {
|
||||
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' }
|
||||
{
|
||||
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();
|
||||
@@ -177,9 +264,9 @@ describe('PersonMultiSelect – click outside', () => {
|
||||
const input = page.getByRole('textbox');
|
||||
await input.fill('Mu');
|
||||
await waitForDebounce();
|
||||
await expect.element(page.getByText('Mustermann, Max')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||||
document.body.click();
|
||||
await tick();
|
||||
await expect.element(page.getByText('Mustermann, Max')).not.toBeInTheDocument();
|
||||
await expect.element(page.getByText('Max Mustermann')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,8 +7,20 @@ 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: '1',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
displayName: 'Max Mustermann',
|
||||
personType: 'PERSON'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
firstName: 'Anna',
|
||||
lastName: 'Musterfrau',
|
||||
displayName: 'Anna Musterfrau',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
];
|
||||
|
||||
function mockFetchWithPersons(persons = PERSONS) {
|
||||
@@ -76,8 +88,8 @@ describe('PersonTypeahead – search', () => {
|
||||
const input = page.getByPlaceholder('Namen tippen...');
|
||||
await input.fill('Mu');
|
||||
await waitForDebounce();
|
||||
await expect.element(page.getByText('Mustermann, Max')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Musterfrau, Anna')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Anna Musterfrau')).toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/person-typeahead-open.png' });
|
||||
});
|
||||
|
||||
@@ -105,7 +117,7 @@ describe('PersonTypeahead – search', () => {
|
||||
const input = page.getByPlaceholder('Namen tippen...');
|
||||
await input.fill('Ma');
|
||||
await waitForDebounce();
|
||||
await expect.element(page.getByText('Mustermann, Max')).not.toBeInTheDocument();
|
||||
await expect.element(page.getByText('Max Mustermann')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,7 +134,7 @@ describe('PersonTypeahead – selection', () => {
|
||||
await tick();
|
||||
await expect.element(input).toHaveValue('Max Mustermann');
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'Mustermann, Max' }))
|
||||
.element(page.getByRole('button', { name: 'Max Mustermann' }))
|
||||
.not.toBeInTheDocument();
|
||||
await page.screenshot({ path: 'test-results/screenshots/person-typeahead-selected.png' });
|
||||
});
|
||||
@@ -152,7 +164,15 @@ describe('PersonTypeahead – selection', () => {
|
||||
});
|
||||
|
||||
it('selects a result with Enter key', async () => {
|
||||
mockFetchWithPersons([{ id: '1', firstName: 'Max', lastName: 'Mustermann' }]);
|
||||
mockFetchWithPersons([
|
||||
{
|
||||
id: '1',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
displayName: 'Max Mustermann',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
]);
|
||||
render(PersonTypeahead, { name: 'senderId', label: 'Absender' });
|
||||
const input = page.getByPlaceholder('Namen tippen...');
|
||||
await input.fill('Ma');
|
||||
@@ -218,7 +238,7 @@ describe('PersonTypeahead – correspondent mode', () => {
|
||||
(document.querySelector('input[placeholder="Namen tippen..."]') as HTMLInputElement).focus();
|
||||
await waitForDebounce();
|
||||
|
||||
await expect.element(page.getByText('Mustermann, Max')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses correspondents endpoint with q param when typing', async () => {
|
||||
@@ -259,9 +279,9 @@ describe('PersonTypeahead – click outside', () => {
|
||||
const input = page.getByPlaceholder('Namen tippen...');
|
||||
await input.fill('Mu');
|
||||
await waitForDebounce();
|
||||
await expect.element(page.getByText('Mustermann, Max')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Max Mustermann')).toBeInTheDocument();
|
||||
document.body.click();
|
||||
await tick();
|
||||
await expect.element(page.getByText('Mustermann, Max')).not.toBeInTheDocument();
|
||||
await expect.element(page.getByText('Max Mustermann')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +54,15 @@ describe('korrespondenz load — senderId set, no receiverId', () => {
|
||||
const docs = [{ id: 'd1', title: 'Testbrief' }];
|
||||
const GET = mockApi([
|
||||
{ ok: true, data: docs },
|
||||
{ ok: true, data: { firstName: 'Hans', lastName: 'Müller' } }
|
||||
{
|
||||
ok: true,
|
||||
data: {
|
||||
firstName: 'Hans',
|
||||
lastName: 'Müller',
|
||||
displayName: 'Hans Müller',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await load({
|
||||
@@ -76,8 +84,24 @@ describe('korrespondenz load — senderId and receiverId set', () => {
|
||||
it('calls conversation, sender person, and receiver person endpoints', async () => {
|
||||
const GET = mockApi([
|
||||
{ ok: true, data: [] },
|
||||
{ ok: true, data: { firstName: 'Hans', lastName: 'Müller' } },
|
||||
{ ok: true, data: { firstName: 'Anna', lastName: 'Schmidt' } }
|
||||
{
|
||||
ok: true,
|
||||
data: {
|
||||
firstName: 'Hans',
|
||||
lastName: 'Müller',
|
||||
displayName: 'Hans Müller',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
},
|
||||
{
|
||||
ok: true,
|
||||
data: {
|
||||
firstName: 'Anna',
|
||||
lastName: 'Schmidt',
|
||||
displayName: 'Anna Schmidt',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await load({
|
||||
@@ -98,7 +122,15 @@ describe('korrespondenz load — canWrite', () => {
|
||||
it('derives canWrite true from WRITE_ALL permission', async () => {
|
||||
mockApi([
|
||||
{ ok: true, data: [] },
|
||||
{ ok: true, data: { firstName: 'Hans', lastName: 'Müller' } }
|
||||
{
|
||||
ok: true,
|
||||
data: {
|
||||
firstName: 'Hans',
|
||||
lastName: 'Müller',
|
||||
displayName: 'Hans Müller',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await load({
|
||||
@@ -113,7 +145,15 @@ describe('korrespondenz load — canWrite', () => {
|
||||
it('derives canWrite false when user lacks WRITE_ALL', async () => {
|
||||
mockApi([
|
||||
{ ok: true, data: [] },
|
||||
{ ok: true, data: { firstName: 'Hans', lastName: 'Müller' } }
|
||||
{
|
||||
ok: true,
|
||||
data: {
|
||||
firstName: 'Hans',
|
||||
lastName: 'Müller',
|
||||
displayName: 'Hans Müller',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await load({
|
||||
@@ -132,7 +172,15 @@ describe('korrespondenz load — backend error', () => {
|
||||
it('throws when the conversation endpoint returns non-ok', async () => {
|
||||
mockApi([
|
||||
{ ok: false, status: 500 },
|
||||
{ ok: true, data: { firstName: 'Hans', lastName: 'Müller' } }
|
||||
{
|
||||
ok: true,
|
||||
data: {
|
||||
firstName: 'Hans',
|
||||
lastName: 'Müller',
|
||||
displayName: 'Hans Müller',
|
||||
personType: 'PERSON'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -58,7 +58,9 @@ describe('New document page – receiver prefill', () => {
|
||||
it('shows a receiver chip when initialReceivers has a person', async () => {
|
||||
const data = {
|
||||
...baseData,
|
||||
initialReceivers: [{ id: 'p2', firstName: 'Anna', lastName: 'Schmidt' }]
|
||||
initialReceivers: [
|
||||
{ id: 'p2', firstName: 'Anna', lastName: 'Schmidt', displayName: 'Anna Schmidt' }
|
||||
]
|
||||
};
|
||||
render(Page, { data, form: null });
|
||||
await expect.element(page.getByText('Anna Schmidt')).toBeInTheDocument();
|
||||
@@ -67,7 +69,9 @@ describe('New document page – receiver prefill', () => {
|
||||
it('renders a hidden receiverIds input for the prefilled receiver', async () => {
|
||||
const data = {
|
||||
...baseData,
|
||||
initialReceivers: [{ id: 'p2', firstName: 'Anna', lastName: 'Schmidt' }]
|
||||
initialReceivers: [
|
||||
{ id: 'p2', firstName: 'Anna', lastName: 'Schmidt', displayName: 'Anna Schmidt' }
|
||||
]
|
||||
};
|
||||
render(Page, { data, form: null });
|
||||
const hidden = document.querySelector<HTMLInputElement>('input[name="receiverIds"]');
|
||||
|
||||
@@ -39,8 +39,22 @@ const makeDoc = (overrides = {}) => ({
|
||||
status: 'UPLOADED' as const,
|
||||
documentDate: '2024-03-15',
|
||||
location: 'Berlin',
|
||||
sender: { id: 'p1', firstName: 'Max', lastName: 'Mustermann' },
|
||||
receivers: [{ id: 'p2', firstName: 'Anna', lastName: 'Musterfrau' }],
|
||||
sender: {
|
||||
id: 'p1',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
displayName: 'Max Mustermann',
|
||||
personType: 'PERSON' as const
|
||||
},
|
||||
receivers: [
|
||||
{
|
||||
id: 'p2',
|
||||
firstName: 'Anna',
|
||||
lastName: 'Musterfrau',
|
||||
displayName: 'Anna Musterfrau',
|
||||
personType: 'PERSON' as const
|
||||
}
|
||||
],
|
||||
tags: [{ id: 't1', name: 'Familie' }],
|
||||
filePath: '/files/testbrief.pdf',
|
||||
createdAt: '2024-03-15T10:00:00Z',
|
||||
|
||||
@@ -11,6 +11,7 @@ const makePerson = (overrides = {}) => ({
|
||||
id: '1',
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
displayName: 'Max Mustermann',
|
||||
documentCount: 0,
|
||||
...overrides
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user