From b8c8fcb1fbb67f0baba8ff36add99ca2997901e4 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 14 Jun 2026 08:34:59 +0200 Subject: [PATCH] fix(timeline): cancel blank-title event save instead of posting The EventForm onsubmit guard called e.preventDefault() on a blank title, but use:enhance ignores defaultPrevented (forms.js only bails on cancel()), so a blank-title Save still fired a network POST. In a component unit test the resulting update() -> applyAction() dereferenced an undefined root ($set on undefined), surfacing as an unhandled rejection. Move the guard into the enhance submit phase and call cancel() so the POST is actually stopped; the server still owns the authoritative fail(400). Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/timeline/EventForm.svelte | 20 +++++++++---------- .../src/lib/timeline/EventForm.svelte.spec.ts | 12 +++++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/frontend/src/lib/timeline/EventForm.svelte b/frontend/src/lib/timeline/EventForm.svelte index 36184adf..3cce8d3e 100644 --- a/frontend/src/lib/timeline/EventForm.svelte +++ b/frontend/src/lib/timeline/EventForm.svelte @@ -110,15 +110,6 @@ function markDirty() { dirty = true; } -// Guards a submit with a blank title client-side. The server re-validates and -// owns the authoritative fail(400) with per-field flags. -function handleSubmit(e: SubmitEvent) { - titleTouched = true; - if (titleEmpty) { - e.preventDefault(); - } -} - async function confirmDelete(e: SubmitEvent) { e.preventDefault(); const { confirm } = getConfirmService(); @@ -153,8 +144,15 @@ async function confirmDelete(e: SubmitEvent) {
{ + use:enhance={({ cancel }) => { + // Client-side guard against a blank title. enhance ignores onsubmit + // preventDefault(), so cancel() is the only thing that actually stops the + // POST; the server still re-validates and owns the authoritative fail(400). + titleTouched = true; + if (titleEmpty) { + cancel(); + return; + } submitting = true; return async ({ update }) => { submitting = false; diff --git a/frontend/src/lib/timeline/EventForm.svelte.spec.ts b/frontend/src/lib/timeline/EventForm.svelte.spec.ts index 4e3a0d96..b473c703 100644 --- a/frontend/src/lib/timeline/EventForm.svelte.spec.ts +++ b/frontend/src/lib/timeline/EventForm.svelte.spec.ts @@ -62,6 +62,18 @@ describe('EventForm — required-field error (REQ-010)', () => { await expect.element(page.getByText('Bitte einen Titel eingeben.')).toBeInTheDocument(); }); + it('cancels the submission — fires no network POST — when title is blank', async () => { + // The client-side guard must actually CANCEL the enhance submission, not just + // show a message: enhance ignores onsubmit preventDefault(), so without cancel() + // a blank-title Save still POSTs (and update()->applyAction crashes with no app). + const fetchSpy = vi.fn(() => new Promise(() => {})); + vi.stubGlobal('fetch', fetchSpy); + render(EventForm, {}); + await page.getByRole('button', { name: 'Speichern' }).click(); + await expect.element(page.getByText('Bitte einen Titel eingeben.')).toBeInTheDocument(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + it('rehydrates the pickers from the fail payload (Decision 6)', async () => { render(EventForm, { form: {