Files
familienarchiv/frontend/src/routes/persons/[id]/PersonCard.svelte.test.ts
Marcel 4dcf8e2242 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 18:14:20 +02:00

190 lines
5.4 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';
afterEach(cleanup);
const basePerson = {
id: 'p-1',
firstName: 'Anna',
lastName: 'Schmidt',
displayName: 'Anna Schmidt',
personType: 'PERSON' as const
};
describe('PersonCard', () => {
it('renders the displayName as the primary heading', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: false } });
await expect.element(page.getByRole('heading', { name: 'Anna Schmidt' })).toBeVisible();
});
it('renders the title above the name when personType is PERSON and title is set', async () => {
render(PersonCard, {
props: { person: { ...basePerson, title: 'Frau Dr.' }, canWrite: false }
});
await expect.element(page.getByText('Frau Dr.')).toBeVisible();
});
it('omits the title for non-PERSON types even if title is set', async () => {
render(PersonCard, {
props: {
person: { ...basePerson, personType: 'INSTITUTION', title: 'Frau Dr.' },
canWrite: false
}
});
await expect.element(page.getByText('Frau Dr.')).not.toBeInTheDocument();
});
it('renders the firstName/lastName initials inside the avatar for PERSON type', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: false } });
await expect.element(page.getByText('AS')).toBeVisible();
});
it('falls back to lastName-only initials when firstName is missing', async () => {
render(PersonCard, {
props: {
person: { ...basePerson, firstName: null, displayName: 'Schmidt' },
canWrite: false
}
});
await expect.element(page.getByText('SS')).toBeVisible();
});
it('renders the PersonTypeBadge for non-PERSON types', async () => {
render(PersonCard, {
props: {
person: { ...basePerson, personType: 'INSTITUTION', displayName: 'Acme Inc.' },
canWrite: false
}
});
await expect.element(page.getByText('Institution')).toBeVisible();
});
it('omits the PersonTypeBadge for PERSON type', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: false } });
await expect.element(page.getByText('Institution')).not.toBeInTheDocument();
await expect.element(page.getByText('Gruppe')).not.toBeInTheDocument();
});
it('renders the alias in italic typography when alias is provided', async () => {
render(PersonCard, {
props: { person: { ...basePerson, alias: 'Annerl' }, canWrite: false }
});
await expect.element(page.getByText(/Annerl/)).toBeVisible();
});
it('renders DAY-precision life dates as full localized dates', async () => {
render(PersonCard, {
props: {
person: {
...basePerson,
birthDate: '1901-03-14',
birthDatePrecision: 'DAY' as const,
deathDate: '1944-11-02',
deathDatePrecision: 'DAY' as const
},
canWrite: false
}
});
await expect.element(page.getByText(/14\. März 1901/)).toBeVisible();
await expect.element(page.getByText(/2\. November 1944/)).toBeVisible();
});
it('wraps the * and † glyphs in aria-hidden spans', async () => {
const { container } = render(PersonCard, {
props: {
person: {
...basePerson,
birthDate: '1901-03-14',
birthDatePrecision: 'DAY' as const,
deathDate: '1944-11-02',
deathDatePrecision: 'DAY' as const
},
canWrite: false
}
});
const hidden = [...container.querySelectorAll('span[aria-hidden="true"]')].map((el) =>
el.textContent?.trim()
);
expect(hidden).toContain('*');
expect(hidden).toContain('†');
});
it('renders birth-only without dash or dagger', async () => {
const { container } = render(PersonCard, {
props: {
person: {
...basePerson,
birthDate: '1901-03-14',
birthDatePrecision: 'DAY' as const
},
canWrite: false
}
});
await expect.element(page.getByText(/14\. März 1901/)).toBeVisible();
expect(container.textContent).not.toContain('');
expect(container.textContent).not.toContain('†');
});
it('renders APPROX-precision legacy dates with the ca. prefix', async () => {
render(PersonCard, {
props: {
person: {
...basePerson,
birthDate: '1882-01-01',
birthDatePrecision: 'APPROX' as const
},
canWrite: false
}
});
await expect.element(page.getByText(/ca\. 1882/)).toBeVisible();
});
it('renders no life-date line when both dates are missing', async () => {
const { container } = render(PersonCard, {
props: { person: basePerson, canWrite: false }
});
expect(container.textContent).not.toContain('*');
expect(container.textContent).not.toContain('†');
});
it('renders the notes section when notes are provided', async () => {
render(PersonCard, {
props: {
person: { ...basePerson, notes: 'Wohnte in Berlin.' },
canWrite: false
}
});
await expect.element(page.getByText('Wohnte in Berlin.')).toBeVisible();
});
it('renders the edit link when canWrite is true', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: true } });
await expect
.element(page.getByRole('link', { name: /bearbeiten/i }))
.toHaveAttribute('href', '/persons/p-1/edit');
});
it('does not render the edit link when canWrite is false', async () => {
render(PersonCard, { props: { person: basePerson, canWrite: false } });
await expect.element(page.getByRole('link', { name: /bearbeiten/i })).not.toBeInTheDocument();
});
});