From 3c99030546a5e34e9251c604346c1040b338ad4d Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 24 Apr 2026 21:18:17 +0200 Subject: [PATCH] fix(bulk-upload): skip navigation when any chunk fails to upload goto('/documents') fired unconditionally, discarding error chips and leaving the user with no feedback on which files failed. Now only navigates when hadErrors is false after all chunks complete. Co-Authored-By: Claude Sonnet 4.6 --- .../document/BulkDocumentEditLayout.svelte | 4 ++- .../BulkDocumentEditLayout.svelte.spec.ts | 27 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte index 594a9245..14e9fe51 100644 --- a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte +++ b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte @@ -92,6 +92,7 @@ async function save() { } chunkProgress = { done: 0, total: chunks.length }; + let hadErrors = false; for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; const formData = new FormData(); @@ -106,6 +107,7 @@ async function save() { formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' })); const res = await fetch('/api/documents/quick-upload', { method: 'POST', body: formData }); if (!res.ok) { + hadErrors = true; const body = await res.json().catch(() => ({ errors: [] })); const errorCount = (body.errors ?? []).length; for (let j = 0; j < errorCount && j < chunk.length; j++) { @@ -115,7 +117,7 @@ async function save() { } chunkProgress = { done: i + 1, total: chunks.length }; } - goto('/documents'); + if (!hadErrors) goto('/documents'); } diff --git a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts index e61bec16..c5dc5e85 100644 --- a/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts +++ b/frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.ts @@ -1,11 +1,15 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; +import { goto } from '$app/navigation'; import { cleanup, render } from 'vitest-browser-svelte'; import { page, userEvent } from 'vitest/browser'; import BulkDocumentEditLayout from './BulkDocumentEditLayout.svelte'; +vi.mock('$app/navigation', () => ({ goto: vi.fn() })); + afterEach(() => { cleanup(); vi.unstubAllGlobals(); + vi.clearAllMocks(); }); function makeFile(name: string): File { @@ -80,9 +84,6 @@ describe('BulkDocumentEditLayout', () => { }); vi.stubGlobal('fetch', mockFetch); - // Also stub goto to prevent navigation errors in test - vi.mock('$app/navigation', () => ({ goto: vi.fn() })); - const { container } = render(BulkDocumentEditLayout, {}); const files = Array.from({ length: 12 }, (_, i) => makeFile(`f${i}.pdf`)); await addFilesViaInput(container, files); @@ -103,7 +104,6 @@ describe('BulkDocumentEditLayout', () => { json: async () => ({ errors: [{ filename: 'f0.pdf', code: 'FILE_UPLOAD_FAILED' }] }) }); vi.stubGlobal('fetch', mockFetch); - vi.mock('$app/navigation', () => ({ goto: vi.fn() })); const { container } = render(BulkDocumentEditLayout, {}); await addFilesViaInput(container, [makeFile('f0.pdf')]); @@ -140,6 +140,25 @@ describe('BulkDocumentEditLayout', () => { expect(metadataJson).toHaveProperty('tagNames'); }); + it('save() does not navigate when chunk returns non-ok response', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + ok: false, + json: async () => ({ errors: [{ filename: 'f0.pdf', code: 'FILE_UPLOAD_FAILED' }] }) + }); + vi.stubGlobal('fetch', mockFetch); + + const { container } = render(BulkDocumentEditLayout, {}); + await addFilesViaInput(container, [makeFile('f0.pdf')]); + + const saveBtn = container.querySelector( + 'button[data-testid="bulk-save-btn"]' + ) as HTMLButtonElement; + saveBtn.click(); + + await vi.waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1), { timeout: 3000 }); + expect(goto).not.toHaveBeenCalled(); + }); + it('discard-all resets to N=0 state and shows drop zone', async () => { const { container } = render(BulkDocumentEditLayout, {}); await addFilesViaInput(container, [makeFile('a.pdf'), makeFile('b.pdf')]);