Files
familienarchiv/frontend/src/lib/person/PersonCard.svelte.test.ts
Marcel 0e7095fee6 feat(person): render precise life dates on cards, hover card, and mention dropdown
Cards compose aria-hidden * / † glyphs in markup so screen readers only
announce the dates; PersonSummaryDTO list card stays year-shaped by
design (ADR-039). MentionDropdown subtitle wraps instead of truncating
so DAY-precision ranges fit at 320px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00

109 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});
// PersonSummaryDTO intentionally stays year-shaped (ADR-039): the list shows
// year precision only; full dates live on the detail page.
it('renders the year-only life-date range', async () => {
render(PersonCard, { props: { person: makePerson({ birthYear: 1899, deathYear: 1972 }) } });
await expect.element(page.getByText(/1899/)).toBeVisible();
await expect.element(page.getByText(/1972/)).toBeVisible();
});
it('renders birth-only without dash and wraps glyphs in aria-hidden spans', async () => {
const { container } = render(PersonCard, {
props: { person: makePerson({ birthYear: 1899 }) }
});
await expect.element(page.getByText(/1899/)).toBeVisible();
expect(container.textContent).not.toContain('');
expect(container.textContent).not.toContain('†');
const hidden = [...container.querySelectorAll('span[aria-hidden="true"]')].map((el) =>
el.textContent?.trim()
);
expect(hidden).toContain('*');
});
});
describe('PersonCard — unconfirmed badge keys off provisional only (badge ⇔ count ⇔ triage parity)', () => {
it('renders without throwing when lastName is null', async () => {
// Before the fix, `lastName[0]` threw at render for a null lastName. Empty-name
// crash-safety is a SEPARATE concern from the badge: the placeholder glyph renders
// regardless, but the "unbestätigt" badge only fires when provisional is true.
const person = makePerson({
lastName: null as unknown as string,
displayName: '?',
provisional: true
});
render(PersonCard, { props: { person } });
// No throw + provisional → the badge is shown.
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('does NOT show the badge for a "?" name when not provisional', async () => {
// Empty/"?" name alone is no longer treated as unconfirmed — only `provisional` is.
// This keeps the badge in lockstep with needsReviewCount and the /persons/review list.
render(PersonCard, {
props: {
person: makePerson({ firstName: undefined, lastName: '?', displayName: '?' })
}
});
await expect.element(page.getByText('unbestätigt')).not.toBeInTheDocument();
});
it('does NOT show the badge for an UNKNOWN type when not provisional', async () => {
render(PersonCard, {
props: { person: makePerson({ personType: 'UNKNOWN', displayName: 'Unklar' }) }
});
await expect.element(page.getByText('unbestätigt')).not.toBeInTheDocument();
});
it('renders the placeholder glyph (never a "?" initial) for an empty name even without provisional', async () => {
// Crash-safety branch: a null/empty lastName must not throw and must not show "?".
render(PersonCard, {
props: {
person: makePerson({
firstName: undefined,
lastName: null as unknown as string,
displayName: 'Unbekannt'
})
}
});
await expect.element(page.getByText('Unbekannt')).toBeVisible();
await expect.element(page.getByText('unbestätigt')).not.toBeInTheDocument();
});
});