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')); // userEvent.tab() / keyboard('{Tab}') do not reliably fire the blur event on // inputs inside Playwright's test iframe. .blur() is a no-op when the element // has lost focus to TipTap's onMount initialisation. Dispatching the FocusEvent // directly fires Svelte's onblur listener regardless of the current focus owner. const input = await page.getByPlaceholder('Titel der Geschichte').element(); input.dispatchEvent(new FocusEvent('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 }); // userEvent.fill() with trailing whitespace does not fire the input event chain // that Svelte's bind:value requires (CDP limitation). Setting .value directly // and dispatching an input event works around this while preserving the trailing // space needed to verify the trim() contract. const input = (await page .getByPlaceholder('Titel der Geschichte') .element()) as HTMLInputElement; input.value = 'My title '; input.dispatchEvent(new Event('input', { bubbles: true })); // userEvent.click() via Playwright CDP does not reliably trigger Svelte 5 onclick // handlers when a TipTap editor is mounted in the same component. Dispatching // the MouseEvent directly from the browser JS context bypasses this issue. const btn = (await page .getByRole('button', { name: 'Entwurf speichern' }) .element()) as HTMLButtonElement; btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); 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'); const btn = (await page .getByRole('button', { name: 'Veröffentlichen' }) .element()) as HTMLButtonElement; btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); 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'); const btn = (await page .getByRole('button', { name: 'Entwurf speichern' }) .element()) as HTMLButtonElement; btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); expect(onSubmit).toHaveBeenCalledTimes(1); const payload = onSubmit.mock.calls[0][0]; expect(payload.personIds).toEqual(['p1']); expect(payload.documentIds).toEqual(['d1']); }); });