fix(timeline): cancel blank-title event save instead of posting
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m35s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 6m14s
CI / fail2ban Regex (pull_request) Successful in 53s
CI / Semgrep Security Scan (pull_request) Successful in 24s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
SDD Gate / RTM Check (pull_request) Successful in 16s
SDD Gate / Contract Validate (pull_request) Successful in 28s
SDD Gate / Constitution Impact (pull_request) Successful in 17s
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m35s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 6m14s
CI / fail2ban Regex (pull_request) Successful in 53s
CI / Semgrep Security Scan (pull_request) Successful in 24s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
SDD Gate / RTM Check (pull_request) Successful in 16s
SDD Gate / Contract Validate (pull_request) Successful in 28s
SDD Gate / Constitution Impact (pull_request) Successful in 17s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -110,15 +110,6 @@ function markDirty() {
|
|||||||
dirty = true;
|
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) {
|
async function confirmDelete(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { confirm } = getConfirmService();
|
const { confirm } = getConfirmService();
|
||||||
@@ -153,8 +144,15 @@ async function confirmDelete(e: SubmitEvent) {
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/save"
|
action="?/save"
|
||||||
onsubmit={handleSubmit}
|
use:enhance={({ cancel }) => {
|
||||||
use:enhance={() => {
|
// 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;
|
submitting = true;
|
||||||
return async ({ update }) => {
|
return async ({ update }) => {
|
||||||
submitting = false;
|
submitting = false;
|
||||||
|
|||||||
@@ -62,6 +62,18 @@ describe('EventForm — required-field error (REQ-010)', () => {
|
|||||||
await expect.element(page.getByText('Bitte einen Titel eingeben.')).toBeInTheDocument();
|
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<Response>(() => {}));
|
||||||
|
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 () => {
|
it('rehydrates the pickers from the fail payload (Decision 6)', async () => {
|
||||||
render(EventForm, {
|
render(EventForm, {
|
||||||
form: {
|
form: {
|
||||||
|
|||||||
Reference in New Issue
Block a user