feat(persons): clean filterable paginated directory with crash fix
Rewrite /persons: server-side filter chips (type, family-only, has-documents) that AND within the clean reader default (familyMember OR documentCount > 0), a writer-only show-all/Zu-prüfen toggle, and reused Pagination. Extract PersonCard (fixes the null-lastName render crash and never shows a "?" initial — provisional/UNKNOWN/"?" entries get a neutral placeholder avatar + a text+icon "unbestätigt" badge, WCAG 1.4.1) and PersonFilterBar (44px aria-pressed chips, role=switch toggle with the count in its accessible name). The loader applies the reader restriction unless review=1 and surfaces a cheap needsReviewCount. i18n keys added for de/en/es. Refs #667 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
66
frontend/src/lib/person/PersonCard.svelte.test.ts
Normal file
66
frontend/src/lib/person/PersonCard.svelte.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import PersonCard from './PersonCard.svelte';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type Person = components['schemas']['PersonSummaryDTO'];
|
||||
|
||||
const makePerson = (overrides: Partial<Person> = {}): Person => ({
|
||||
id: 'p-1',
|
||||
firstName: 'Anna',
|
||||
lastName: 'Schmidt',
|
||||
displayName: 'Anna Schmidt',
|
||||
personType: 'PERSON',
|
||||
familyMember: false,
|
||||
provisional: false,
|
||||
documentCount: 0,
|
||||
...overrides
|
||||
});
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('PersonCard — confirmed person', () => {
|
||||
it('renders the display name', async () => {
|
||||
render(PersonCard, { props: { person: makePerson() } });
|
||||
await expect.element(page.getByText('Anna Schmidt')).toBeVisible();
|
||||
});
|
||||
|
||||
it('does not show an unconfirmed badge for a confirmed person', async () => {
|
||||
render(PersonCard, { props: { person: makePerson() } });
|
||||
await expect.element(page.getByText('unbestätigt')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PersonCard — unconfirmed / malformed (regression: null-lastName crash)', () => {
|
||||
it('renders without throwing when lastName is null', async () => {
|
||||
// Before the fix, `lastName[0]` threw at render for a null lastName.
|
||||
const person = makePerson({
|
||||
lastName: null as unknown as string,
|
||||
displayName: '?',
|
||||
provisional: true
|
||||
});
|
||||
render(PersonCard, { props: { person } });
|
||||
// No throw + the placeholder avatar (an <img>) is present, never a "?" initial.
|
||||
await expect.element(page.getByText('unbestätigt')).toBeVisible();
|
||||
});
|
||||
|
||||
it('shows an unbestätigt badge for a provisional person', async () => {
|
||||
render(PersonCard, { props: { person: makePerson({ provisional: true }) } });
|
||||
await expect.element(page.getByText('unbestätigt')).toBeVisible();
|
||||
});
|
||||
|
||||
it('shows a placeholder (no "?" initial) for a "?" name', async () => {
|
||||
render(PersonCard, {
|
||||
props: { person: makePerson({ firstName: undefined, lastName: '?', displayName: '?' }) }
|
||||
});
|
||||
await expect.element(page.getByText('unbestätigt')).toBeVisible();
|
||||
});
|
||||
|
||||
it('treats an UNKNOWN type as unconfirmed', async () => {
|
||||
render(PersonCard, {
|
||||
props: { person: makePerson({ personType: 'UNKNOWN', displayName: 'Unklar' }) }
|
||||
});
|
||||
await expect.element(page.getByText('unbestätigt')).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user