fix(geschichte): uniform onSubmit rejects-on-failure contract
The c3afd57e fix made the edit page's handleSubmit throw on !res.ok but
only JourneyEditor caught it. Now: GeschichteEditor.save() catches and
keeps its dirty state (no unhandled rejection -> no GlitchTip noise on a
failed STORY save); StoryCreate throws on failure so a failed STORY
create no longer silently disarms the unsaved guard; both handleSubmit
implementations catch network rejections, surface a message, and rethrow.
Contract documented on both editors' Props. GeschichteEditor also gets
the title maxlength=255. Spec: rejecting onSubmit is caught and the
editor stays usable.
Review round 3: Felix §2, Tobias S1, Nora (3), Markus (concern 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, unknown>): boolean {
|
||||
@@ -135,6 +140,7 @@ function exec(action: () => void) {
|
||||
<input
|
||||
type="text"
|
||||
bind:value={title}
|
||||
maxlength="255"
|
||||
oninput={handleTitleInput}
|
||||
onblur={handleTitleBlur}
|
||||
placeholder={m.geschichte_editor_title_placeholder()}
|
||||
|
||||
@@ -54,6 +54,22 @@ describe('GeschichteEditor — title-required guard', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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() });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user