From 48649e67f90e5f6d8d166ecaa2446ac733277585 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 28 Apr 2026 10:41:51 +0200 Subject: [PATCH] refactor(stammbaum): extract RelationshipChip and AddRelationshipForm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split StammbaumCard from 366 to 196 lines by extracting: - RelationshipChip.svelte — single relationship list item with optional delete - AddRelationshipForm.svelte — self-contained add-relationship form with open/close state Both components have browser-mode spec tests covering rendering and interaction. Co-Authored-By: Claude Sonnet 4.6 --- .../lib/components/AddRelationshipForm.svelte | 161 +++++++++++++++ .../AddRelationshipForm.svelte.spec.ts | 40 ++++ .../lib/components/RelationshipChip.svelte | 49 +++++ .../RelationshipChip.svelte.spec.ts | 48 +++++ .../src/lib/components/StammbaumCard.svelte | 189 +----------------- 5 files changed, 308 insertions(+), 179 deletions(-) create mode 100644 frontend/src/lib/components/AddRelationshipForm.svelte create mode 100644 frontend/src/lib/components/AddRelationshipForm.svelte.spec.ts create mode 100644 frontend/src/lib/components/RelationshipChip.svelte create mode 100644 frontend/src/lib/components/RelationshipChip.svelte.spec.ts diff --git a/frontend/src/lib/components/AddRelationshipForm.svelte b/frontend/src/lib/components/AddRelationshipForm.svelte new file mode 100644 index 00000000..0bb7fb8b --- /dev/null +++ b/frontend/src/lib/components/AddRelationshipForm.svelte @@ -0,0 +1,161 @@ + + +{#if !open} + +{:else} +
{ + return async ({ result, update }) => { + await update(); + if (result.type === 'success') { + open = false; + reset(); + } + }; + }} + class="mt-3 rounded-sm border border-line bg-muted/40 p-3" + > +
+ +
+ +
+ + +
+ {#if selfError} + + {/if} +
+ + +
+
+{/if} diff --git a/frontend/src/lib/components/AddRelationshipForm.svelte.spec.ts b/frontend/src/lib/components/AddRelationshipForm.svelte.spec.ts new file mode 100644 index 00000000..5dec75e3 --- /dev/null +++ b/frontend/src/lib/components/AddRelationshipForm.svelte.spec.ts @@ -0,0 +1,40 @@ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import AddRelationshipForm from './AddRelationshipForm.svelte'; + +vi.mock('$app/forms', () => ({ enhance: () => () => {} })); +vi.mock('$lib/components/PersonTypeahead.svelte', () => ({ default: () => null })); + +afterEach(cleanup); + +describe('AddRelationshipForm', () => { + it('shows add-relationship button initially and no form', async () => { + render(AddRelationshipForm, { personId: 'person-1' }); + await expect.element(page.getByRole('button')).toBeInTheDocument(); + await expect.element(page.getByRole('combobox')).not.toBeInTheDocument(); + }); + + it('shows relationType select when add button is clicked', async () => { + render(AddRelationshipForm, { personId: 'person-1' }); + document.querySelector('button')!.click(); + await expect.element(page.getByRole('combobox')).toBeInTheDocument(); + }); + + it('hides form and shows button when cancel is clicked', async () => { + render(AddRelationshipForm, { personId: 'person-1' }); + document.querySelector('button')!.click(); + await expect.element(page.getByRole('combobox')).toBeInTheDocument(); + const cancelBtn = [...document.querySelectorAll('button')].find( + (b) => b.type === 'button' && /abbrechen/i.test(b.textContent ?? '') + ); + cancelBtn!.click(); + await expect.element(page.getByRole('combobox')).not.toBeInTheDocument(); + }); + + it('submit is disabled when no person is selected', async () => { + render(AddRelationshipForm, { personId: 'person-1' }); + document.querySelector('button')!.click(); + await expect.element(page.getByRole('button', { name: /^Hinzufügen$/i })).toBeDisabled(); + }); +}); diff --git a/frontend/src/lib/components/RelationshipChip.svelte b/frontend/src/lib/components/RelationshipChip.svelte new file mode 100644 index 00000000..7d895aba --- /dev/null +++ b/frontend/src/lib/components/RelationshipChip.svelte @@ -0,0 +1,49 @@ + + +
  • + + {chipLabel} + + + {otherName} + + {#if yearRange} + {yearRange} + {/if} + {#if canWrite} +
    + + +
    + {/if} +
  • diff --git a/frontend/src/lib/components/RelationshipChip.svelte.spec.ts b/frontend/src/lib/components/RelationshipChip.svelte.spec.ts new file mode 100644 index 00000000..2ee57ccc --- /dev/null +++ b/frontend/src/lib/components/RelationshipChip.svelte.spec.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import RelationshipChip from './RelationshipChip.svelte'; + +vi.mock('$app/forms', () => ({ enhance: () => () => {} })); + +afterEach(cleanup); + +const baseProps = { + chipLabel: 'Elternteil', + otherName: 'Anna Schmidt', + yearRange: '', + canWrite: false, + relId: 'rel-1' +}; + +describe('RelationshipChip', () => { + it('renders the chip label', async () => { + render(RelationshipChip, baseProps); + await expect.element(page.getByText('Elternteil')).toBeInTheDocument(); + }); + + it('renders the other person name', async () => { + render(RelationshipChip, baseProps); + await expect.element(page.getByText('Anna Schmidt')).toBeInTheDocument(); + }); + + it('shows year range when provided', async () => { + render(RelationshipChip, { ...baseProps, yearRange: '1920–1980' }); + await expect.element(page.getByText('1920–1980')).toBeInTheDocument(); + }); + + it('does not show year range span when empty', async () => { + render(RelationshipChip, { ...baseProps, yearRange: '' }); + expect(document.querySelector('[data-testid="year-range"]')).toBeNull(); + }); + + it('shows delete button when canWrite is true', async () => { + render(RelationshipChip, { ...baseProps, canWrite: true }); + await expect.element(page.getByRole('button')).toBeInTheDocument(); + }); + + it('hides delete button when canWrite is false', async () => { + render(RelationshipChip, { ...baseProps, canWrite: false }); + expect(document.querySelector('button')).toBeNull(); + }); +}); diff --git a/frontend/src/lib/components/StammbaumCard.svelte b/frontend/src/lib/components/StammbaumCard.svelte index 9c7b562b..af54215f 100644 --- a/frontend/src/lib/components/StammbaumCard.svelte +++ b/frontend/src/lib/components/StammbaumCard.svelte @@ -1,7 +1,8 @@
    @@ -186,156 +154,19 @@ function resetAddForm() { {:else}
      {#each sortedDirect as rel (rel.id)} -
    • - - {chipLabel(rel)} - - - {otherName(rel)} - - {#if yearRange(rel)} - {yearRange(rel)} - {/if} - {#if canWrite} -
      - - -
      - {/if} -
    • + {/each}
    {/if} - {#if canWrite} - {#if !addFormOpen} - - {:else} -
    { - return async ({ result, update }) => { - await update(); - if (result.type === 'success') { - addFormOpen = false; - resetAddForm(); - } - }; - }} - class="mt-3 rounded-sm border border-line bg-muted/40 p-3" - > -
    - -
    - -
    - - -
    - {#if selfError} - - {/if} -
    - - -
    -
    - {/if} + {/if}