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>
191 lines
5.6 KiB
TypeScript
191 lines
5.6 KiB
TypeScript
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);
|
||
});
|
||
});
|