diff --git a/frontend/src/lib/geschichte/GeschichteEditor.svelte b/frontend/src/lib/geschichte/GeschichteEditor.svelte index b3b5a9b6..3a562427 100644 --- a/frontend/src/lib/geschichte/GeschichteEditor.svelte +++ b/frontend/src/lib/geschichte/GeschichteEditor.svelte @@ -14,6 +14,7 @@ type Person = components['schemas']['Person']; interface Props { geschichte?: GeschichteView | null; initialPersons?: Person[]; + /** Must reject when the save failed — the editor keeps its dirty state then. */ onSubmit: (payload: { title: string; body: string; @@ -106,13 +107,17 @@ function handleTitleInput() { async function save(nextStatus: 'DRAFT' | 'PUBLISHED') { titleTouched = true; if (titleEmpty) return; - await onSubmit({ - title: title.trim(), - body, - status: nextStatus, - personIds: selectedPersons.map((p) => p.id!).filter(Boolean) - }); - dirty = false; + try { + await onSubmit({ + title: title.trim(), + body, + status: nextStatus, + personIds: selectedPersons.map((p) => p.id!).filter(Boolean) + }); + dirty = false; + } catch { + // onSubmit signalled failure — keep dirty so the unsaved guard stays armed + } } function isActive(name: string, attrs?: Record): boolean { @@ -135,6 +140,7 @@ function exec(action: () => void) { { }); }); +describe('GeschichteEditor — onSubmit rejects on failure', () => { + it('catches a rejecting onSubmit (no unhandled rejection) and stays editable', async () => { + // Contract: onSubmit rejects on failure. Without the catch in save(), this + // click would surface as an unhandled promise rejection and fail the run. + const onSubmit = vi.fn().mockRejectedValue(new Error('save failed')); + render(GeschichteEditor, { geschichte: draftFactory(), onSubmit }); + + await userEvent.click(page.getByRole('button', { name: 'Entwurf speichern' })); + await vi.waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); + + // Editor still functional — a second save attempt goes through + await userEvent.click(page.getByRole('button', { name: 'Entwurf speichern' })); + await vi.waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(2)); + }); +}); + describe('GeschichteEditor — save bar adapts to status', () => { it('renders DRAFT mode buttons when no geschichte prop is supplied', async () => { render(GeschichteEditor, { onSubmit: vi.fn() }); diff --git a/frontend/src/routes/geschichten/[id]/edit/+page.svelte b/frontend/src/routes/geschichten/[id]/edit/+page.svelte index 3b77c83c..59f4b8b1 100644 --- a/frontend/src/routes/geschichten/[id]/edit/+page.svelte +++ b/frontend/src/routes/geschichten/[id]/edit/+page.svelte @@ -35,6 +35,14 @@ async function handleSubmit(payload: { throw new Error('save failed'); } goto(`/geschichten/${data.geschichte.id}`); + } catch (e) { + if (!errorMessage) { + console.error('Geschichte save failed', e); + errorMessage = getErrorMessage(undefined); + } + // Contract: onSubmit rejects on failure — both editors catch and keep + // their dirty state instead of disarming the unsaved-changes guard. + throw e; } finally { submitting = false; } diff --git a/frontend/src/routes/geschichten/new/StoryCreate.svelte b/frontend/src/routes/geschichten/new/StoryCreate.svelte index 5bd49fdc..58d76083 100644 --- a/frontend/src/routes/geschichten/new/StoryCreate.svelte +++ b/frontend/src/routes/geschichten/new/StoryCreate.svelte @@ -31,10 +31,18 @@ async function handleSubmit(payload: { if (!res.ok) { const code = (await res.json().catch(() => ({})))?.code; errorMessage = getErrorMessage(code); - return; + throw new Error('create failed'); } const created = await res.json(); goto(`/geschichten/${created.id}`); + } catch (e) { + if (!errorMessage) { + console.error('Story create failed', e); + errorMessage = getErrorMessage(undefined); + } + // Contract: onSubmit rejects on failure — GeschichteEditor catches and + // keeps its dirty state instead of disarming the unsaved-changes guard. + throw e; } finally { submitting = false; }