fix(geschichte): handle DELETE failure — show inline error on non-ok response
Adds deleteError $state to [id]/+page.svelte, parses backend error via parseBackendError/getErrorMessage on !res.ok, and displays a role=alert paragraph. Adds two browser-tier tests: success path (goto called) and error path (alert visible, goto not called). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { formatDate } from '$lib/shared/utils/date';
|
||||
import { formatAuthorDisplayName } from '$lib/geschichte/utils';
|
||||
import { getConfirmService } from '$lib/shared/services/confirm.svelte';
|
||||
import { csrfFetch } from '$lib/shared/cookies';
|
||||
import { parseBackendError, getErrorMessage } from '$lib/shared/errors';
|
||||
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
||||
import StoryReader from '$lib/geschichte/StoryReader.svelte';
|
||||
import JourneyReader from '$lib/geschichte/JourneyReader.svelte';
|
||||
@@ -24,7 +25,10 @@ const authorName = $derived(formatAuthorDisplayName(g.author));
|
||||
|
||||
const confirm = getConfirmService();
|
||||
|
||||
let deleteError = $state<string | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
deleteError = null;
|
||||
const ok = await confirm.confirm({
|
||||
title: m.geschichte_delete_confirm_title(),
|
||||
body: m.geschichte_delete_confirm_body(),
|
||||
@@ -36,6 +40,9 @@ async function handleDelete() {
|
||||
const res = await csrfFetch(`/api/geschichten/${g.id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
goto('/geschichten');
|
||||
} else {
|
||||
const err = await parseBackendError(res);
|
||||
deleteError = getErrorMessage(err?.code);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -65,6 +72,15 @@ async function handleDelete() {
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{#if deleteError}
|
||||
<p
|
||||
role="alert"
|
||||
class="mb-4 rounded border border-danger/30 bg-danger/10 px-4 py-3 font-sans text-sm text-danger"
|
||||
>
|
||||
{deleteError}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if isJourney}
|
||||
<JourneyReader geschichte={g} canBlogWrite={data.canBlogWrite} ondelete={handleDelete} />
|
||||
{:else}
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
|
||||
vi.mock('$app/navigation', () => ({
|
||||
beforeNavigate: () => {},
|
||||
afterNavigate: () => {},
|
||||
goto: vi.fn(),
|
||||
invalidate: vi.fn(),
|
||||
invalidateAll: vi.fn(),
|
||||
preloadCode: vi.fn(),
|
||||
preloadData: vi.fn(),
|
||||
pushState: vi.fn(),
|
||||
replaceState: vi.fn(),
|
||||
disableScrollHandling: vi.fn(),
|
||||
onNavigate: () => () => {}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/shared/cookies', () => ({
|
||||
csrfFetch: vi.fn()
|
||||
}));
|
||||
|
||||
import { createConfirmService, CONFIRM_KEY } from '$lib/shared/services/confirm.svelte.js';
|
||||
import { csrfFetch } from '$lib/shared/cookies';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
const { default: GeschichtePage } = await import('./+page.svelte');
|
||||
@@ -205,4 +225,50 @@ describe('geschichten/[id] page', () => {
|
||||
await expect.element(page.getByText(/Im Jahr 1923/)).toBeVisible();
|
||||
await expect.element(page.getByText(/Diese Lesereise ist noch leer/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('delete success: navigates to /geschichten after confirmed DELETE returns ok', async () => {
|
||||
vi.mocked(csrfFetch).mockResolvedValue(new Response(null, { status: 200 }));
|
||||
const confirmService = createConfirmService();
|
||||
render(GeschichtePage, {
|
||||
context: new Map([[CONFIRM_KEY, confirmService]]),
|
||||
props: { data: baseData({ canBlogWrite: true }) }
|
||||
});
|
||||
|
||||
// Trigger delete — opens confirm dialog
|
||||
const deleteBtn = page.getByRole('button', { name: /löschen/i });
|
||||
userEvent.click(deleteBtn);
|
||||
|
||||
// Settle the confirmation dialog
|
||||
confirmService.settle(true);
|
||||
|
||||
// Wait for the async delete to complete, then check goto was called
|
||||
await vi.waitFor(() => {
|
||||
expect(vi.mocked(goto)).toHaveBeenCalledWith('/geschichten');
|
||||
});
|
||||
});
|
||||
|
||||
it('delete failure: shows error message when DELETE returns non-ok', async () => {
|
||||
vi.mocked(csrfFetch).mockResolvedValue(
|
||||
new Response(JSON.stringify({ code: 'FORBIDDEN' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
);
|
||||
const confirmService = createConfirmService();
|
||||
render(GeschichtePage, {
|
||||
context: new Map([[CONFIRM_KEY, confirmService]]),
|
||||
props: { data: baseData({ canBlogWrite: true }) }
|
||||
});
|
||||
|
||||
// Trigger delete — opens confirm dialog
|
||||
const deleteBtn = page.getByRole('button', { name: /löschen/i });
|
||||
userEvent.click(deleteBtn);
|
||||
|
||||
// Settle the confirmation dialog
|
||||
confirmService.settle(true);
|
||||
|
||||
// Wait for the error to appear inline
|
||||
await expect.element(page.getByRole('alert')).toBeVisible();
|
||||
expect(vi.mocked(goto)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user