diff --git a/frontend/src/routes/geschichten/[id]/+page.svelte b/frontend/src/routes/geschichten/[id]/+page.svelte index 7297ee66..b691bc0e 100644 --- a/frontend/src/routes/geschichten/[id]/+page.svelte +++ b/frontend/src/routes/geschichten/[id]/+page.svelte @@ -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(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); } } @@ -65,6 +72,15 @@ async function handleDelete() {

+ {#if deleteError} + + {/if} + {#if isJourney} {:else} diff --git a/frontend/src/routes/geschichten/[id]/page.svelte.test.ts b/frontend/src/routes/geschichten/[id]/page.svelte.test.ts index 10172d12..6dc213a4 100644 --- a/frontend/src/routes/geschichten/[id]/page.svelte.test.ts +++ b/frontend/src/routes/geschichten/[id]/page.svelte.test.ts @@ -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(); + }); });