From c23fad7dc8a1ff002a66df3fd1f5fa68646275df Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 2 May 2026 18:51:40 +0200 Subject: [PATCH] test(geschichten): cover GeschichteEditor title guard, status mode, pre-fill, payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10 browser-based component tests: - title-empty disables both DRAFT save buttons - inline title-required error appears after blur - DRAFT mode renders "Entwurf speichern" + "Veröffentlichen" - PUBLISHED mode renders "Speichern" + "Zurück zu Entwurf" - initialPersons / initialDocuments props render as chips on first paint - title input is populated from a geschichte prop - "Entwurf speichern" passes trimmed title + status=DRAFT to onSubmit - "Veröffentlichen" passes status=PUBLISHED - personIds / documentIds from initial props flow through onSubmit Closes Felix's review B1 on PR #382. Co-Authored-By: Claude Opus 4.7 --- .../GeschichteEditor.svelte.spec.ts | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 frontend/src/lib/components/GeschichteEditor.svelte.spec.ts diff --git a/frontend/src/lib/components/GeschichteEditor.svelte.spec.ts b/frontend/src/lib/components/GeschichteEditor.svelte.spec.ts new file mode 100644 index 00000000..9207deab --- /dev/null +++ b/frontend/src/lib/components/GeschichteEditor.svelte.spec.ts @@ -0,0 +1,150 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page, userEvent } from 'vitest/browser'; +import GeschichteEditor from './GeschichteEditor.svelte'; + +const personFactory = (id: string, displayName: string) => ({ + id, + firstName: displayName.split(' ')[0], + lastName: displayName.split(' ').slice(1).join(' ') || displayName, + displayName, + personType: 'PERSON' as const +}); + +const docFactory = (id: string, title: string, date = '1882-01-01') => ({ + id, + title, + documentDate: date, + originalFilename: `${title}.pdf`, + status: 'UPLOADED' as const, + metadataComplete: false, + scriptType: 'UNKNOWN' as const, + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00' +}); + +const draftFactory = (overrides: Record = {}) => ({ + id: 'g1', + title: 'Existing draft', + body: '

Hello world

', + status: 'DRAFT' as const, + persons: [], + documents: [], + createdAt: '2024-01-01T00:00:00', + updatedAt: '2024-01-01T00:00:00', + ...overrides +}); + +afterEach(() => cleanup()); + +describe('GeschichteEditor — title-required guard', () => { + it('disables both DRAFT save buttons when the title is empty', async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + render(GeschichteEditor, { onSubmit }); + + const draft = await page.getByRole('button', { name: 'Entwurf speichern' }).element(); + const publish = await page.getByRole('button', { name: 'Veröffentlichen' }).element(); + expect(draft).toHaveProperty('disabled', true); + expect(publish).toHaveProperty('disabled', true); + }); + + it('shows the inline error after the title field is blurred while empty', async () => { + const onSubmit = vi.fn(); + render(GeschichteEditor, { onSubmit }); + + await userEvent.click(page.getByPlaceholder('Titel der Geschichte')); + await userEvent.tab(); // blur + await expect.element(page.getByText('Bitte gib einen Titel ein.')).toBeInTheDocument(); + }); +}); + +describe('GeschichteEditor — save bar adapts to status', () => { + it('renders DRAFT mode buttons when no geschichte prop is supplied', async () => { + render(GeschichteEditor, { onSubmit: vi.fn() }); + await expect + .element(page.getByRole('button', { name: 'Entwurf speichern' })) + .toBeInTheDocument(); + await expect.element(page.getByRole('button', { name: 'Veröffentlichen' })).toBeInTheDocument(); + }); + + it('renders PUBLISHED mode buttons when geschichte.status is PUBLISHED', async () => { + render(GeschichteEditor, { + geschichte: draftFactory({ status: 'PUBLISHED', publishedAt: '2024-04-01T12:00:00' }), + onSubmit: vi.fn() + }); + await expect.element(page.getByRole('button', { name: 'Speichern' })).toBeInTheDocument(); + await expect + .element(page.getByRole('button', { name: 'Zurück zu Entwurf' })) + .toBeInTheDocument(); + }); +}); + +describe('GeschichteEditor — pre-fill', () => { + it('renders initial persons as chips', async () => { + render(GeschichteEditor, { + initialPersons: [personFactory('p1', 'Franz Raddatz')], + onSubmit: vi.fn() + }); + await expect.element(page.getByText('Franz Raddatz')).toBeInTheDocument(); + }); + + it('renders initial documents as chips', async () => { + render(GeschichteEditor, { + initialDocuments: [docFactory('d1', 'Brief von Eugenie')], + onSubmit: vi.fn() + }); + await expect.element(page.getByText(/Brief von Eugenie/)).toBeInTheDocument(); + }); + + it('populates the title input from a geschichte prop', async () => { + render(GeschichteEditor, { + geschichte: draftFactory({ title: 'My existing story' }), + onSubmit: vi.fn() + }); + const input = await page.getByPlaceholder('Titel der Geschichte').element(); + expect((input as HTMLInputElement).value).toBe('My existing story'); + }); +}); + +describe('GeschichteEditor — onSubmit payload', () => { + it('passes the trimmed title and DRAFT status when "Entwurf speichern" is clicked', async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + render(GeschichteEditor, { onSubmit }); + + await userEvent.fill(page.getByPlaceholder('Titel der Geschichte'), ' My title '); + await userEvent.click(page.getByRole('button', { name: 'Entwurf speichern' })); + + expect(onSubmit).toHaveBeenCalledTimes(1); + const payload = onSubmit.mock.calls[0][0]; + expect(payload.title).toBe('My title'); + expect(payload.status).toBe('DRAFT'); + }); + + it('passes status=PUBLISHED when "Veröffentlichen" is clicked', async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + render(GeschichteEditor, { onSubmit }); + + await userEvent.fill(page.getByPlaceholder('Titel der Geschichte'), 'Story'); + await userEvent.click(page.getByRole('button', { name: 'Veröffentlichen' })); + + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit.mock.calls[0][0].status).toBe('PUBLISHED'); + }); + + it('passes the personIds and documentIds from initial props through onSubmit', async () => { + const onSubmit = vi.fn().mockResolvedValue(undefined); + render(GeschichteEditor, { + initialPersons: [personFactory('p1', 'Franz Raddatz')], + initialDocuments: [docFactory('d1', 'Brief A')], + onSubmit + }); + + await userEvent.fill(page.getByPlaceholder('Titel der Geschichte'), 'Story'); + await userEvent.click(page.getByRole('button', { name: 'Entwurf speichern' })); + + expect(onSubmit).toHaveBeenCalledTimes(1); + const payload = onSubmit.mock.calls[0][0]; + expect(payload.personIds).toEqual(['p1']); + expect(payload.documentIds).toEqual(['d1']); + }); +});