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>
109 lines
4.1 KiB
TypeScript
109 lines
4.1 KiB
TypeScript
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();
|
||
});
|
||
});
|