Files
familienarchiv/frontend/src/lib/person/genealogy/StammbaumCard.svelte.test.ts
Marcel 491d1a015a feat(relationship): date+precision edit UI, notes, and read-view display
Regenerate api.ts for the LocalDate+DatePrecision RelationshipDTO /
RelationshipUpsertRequest and the new PUT, then migrate every caller:

- RelationshipDateField (mirrors PersonLifeDateField: DAY/MONTH/YEAR, 44px
  targets, labelled, semantic dark-mode tokens, relation_* i18n keys).
- AddRelationshipForm is now upsert-capable: an optional `relationship` prop
  pre-fills type, person, both dates+precision and notes; posts to
  ?/updateRelationship (else ?/addRelationship); the submit control disables and
  shows a progress spinner while a request is in flight (REQ-019); notes textarea
  (<=2000).
- RelationshipChip gains an accessible Edit affordance (canWrite + onEdit);
  StammbaumCard wires it, formats the date range via formatRelationshipDateRange,
  and sorts by fromDate. PersonRelationshipsCard (read view) shows the date range
  and notes; no dates -> no date line.
- persons/[id]/edit/+page.server.ts: updateRelationship action (PUT) + the
  addRelationship action reshaped to date+precision+notes (empty date omits
  precision for coherence).
- Genealogy callers fixed for the dropped year fields: familyForest spouse-order
  and StammbaumConnectors ended-edge dashing now key off fromDate/toDate.
- i18n relation_* form keys in de/en/es.

REQ-004, REQ-014, REQ-015, REQ-016, REQ-019

Refs #837
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:24:24 +02:00

191 lines
5.6 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 StammbaumCard from './StammbaumCard.svelte';
afterEach(cleanup);
const baseProps = (overrides: Record<string, unknown> = {}) => ({
personId: 'p-1',
familyMember: false,
relationships: [] as unknown[],
inferredRelationships: [] as unknown[],
canWrite: false,
relationshipError: null as string | null,
...overrides
});
describe('StammbaumCard', () => {
it('renders the heading', async () => {
render(StammbaumCard, { props: baseProps() });
await expect
.element(page.getByRole('heading', { name: /stammbaum & beziehungen/i }))
.toBeVisible();
});
it('renders the family-member toggle when canWrite is true', async () => {
render(StammbaumCard, { props: baseProps({ canWrite: true }) });
await expect.element(page.getByRole('switch')).toBeVisible();
});
it('omits the family-member toggle when canWrite is false', async () => {
render(StammbaumCard, { props: baseProps() });
await expect.element(page.getByRole('switch')).not.toBeInTheDocument();
});
it('marks the toggle as aria-checked=true when familyMember is true', async () => {
render(StammbaumCard, { props: baseProps({ canWrite: true, familyMember: true }) });
await expect.element(page.getByRole('switch')).toHaveAttribute('aria-checked', 'true');
});
it('renders the in-tree banner when familyMember is true', async () => {
render(StammbaumCard, { props: baseProps({ familyMember: true }) });
await expect.element(page.getByText('Erscheint im Stammbaum')).toBeVisible();
await expect
.element(page.getByRole('link', { name: /ansehen/i }))
.toHaveAttribute('href', '/stammbaum?focus=p-1');
});
it('hides the in-tree banner when familyMember is false', async () => {
render(StammbaumCard, { props: baseProps() });
await expect.element(page.getByText('Erscheint im Stammbaum')).not.toBeInTheDocument();
});
it('shows the relationshipError alert when set', async () => {
render(StammbaumCard, {
props: baseProps({ relationshipError: 'Beziehung konnte nicht gespeichert werden.' })
});
await expect
.element(page.getByText('Beziehung konnte nicht gespeichert werden.'))
.toBeVisible();
});
it('renders the empty placeholder for direct relationships when none exist', async () => {
render(StammbaumCard, { props: baseProps() });
await expect.element(page.getByText('Noch keine Beziehungen bekannt.')).toBeVisible();
});
it('hides the inferred-relationships disclosure when there are none', async () => {
render(StammbaumCard, { props: baseProps() });
await expect.element(page.getByText('Abgeleitete Beziehungen')).not.toBeInTheDocument();
});
it('renders the AddRelationshipForm when canWrite is true', async () => {
render(StammbaumCard, { props: baseProps({ canWrite: true }) });
// AddRelationshipForm renders interactive elements
const buttons = document.querySelectorAll('button');
expect(buttons.length).toBeGreaterThan(1);
});
it('renders direct relationships sorted by relationType order', async () => {
render(StammbaumCard, {
props: baseProps({
relationships: [
{
id: 'r-friend',
relationType: 'FRIEND',
personA: { id: 'p-1', displayName: 'Anna' },
personB: { id: 'p-friend', displayName: 'Carlos' }
},
{
id: 'r-parent',
relationType: 'PARENT_OF',
personA: { id: 'p-1', displayName: 'Anna' },
personB: { id: 'p-child', displayName: 'Daniel' }
}
]
})
});
const items = document.querySelectorAll('ul.divide-y > li, ul.divide-y > *');
expect(items.length).toBeGreaterThanOrEqual(2);
});
it('renders the date range "from to" for a relationship with both dates', async () => {
render(StammbaumCard, {
props: baseProps({
relationships: [
{
id: 'r-1',
personId: 'p-1',
relatedPersonId: 'p-x',
personDisplayName: 'Anna',
relatedPersonDisplayName: 'Xavier',
relationType: 'COLLEAGUE',
fromDate: '1940-01-01',
fromDatePrecision: 'YEAR',
toDate: '1945-01-01',
toDatePrecision: 'YEAR'
}
]
})
});
expect(document.body.textContent).toContain('1940');
expect(document.body.textContent).toContain('1945');
});
it('renders only the start date for a relationship with no end date', async () => {
render(StammbaumCard, {
props: baseProps({
relationships: [
{
id: 'r-2',
personId: 'p-1',
relatedPersonId: 'p-y',
personDisplayName: 'Anna',
relatedPersonDisplayName: 'Yvonne',
relationType: 'NEIGHBOR',
fromDate: '1935-01-01',
fromDatePrecision: 'YEAR',
toDatePrecision: 'UNKNOWN'
}
]
})
});
expect(document.body.textContent).toContain('1935');
expect(document.body.textContent).not.toContain('1935 ');
});
it('renders the inferred-relationships disclosure when topDerived has items', async () => {
render(StammbaumCard, {
props: baseProps({
inferredRelationships: [
{
label: 'GRANDPARENT_OF',
person: { id: 'p-grand', displayName: 'Grandma' }
}
]
})
});
await expect.element(page.getByText('Abgeleitete Beziehungen')).toBeVisible();
expect(document.body.textContent).toContain('Grandma');
});
it('caps the inferred relationships at 5 items', async () => {
const inferred = Array.from({ length: 8 }, (_, i) => ({
label: 'COUSIN_OF',
person: { id: `p-cousin-${i}`, displayName: `Cousin ${i}` }
}));
render(StammbaumCard, {
props: baseProps({ inferredRelationships: inferred })
});
const items = document.querySelectorAll('details ul > li');
expect(items.length).toBe(5);
});
});