Files
patrickandClaude Sonnet 5 6d1130e23e DOC-03: drag-and-drop-upload-oberflaeche
Next.js/React/TypeScript auf web/shl (SHL-01), Next.js/Test-Aufbau nach
dem Muster von web/pdf-viewer (DOC-05). lib/chunkUpload.ts laedt Dateien
in 4-MiB-Chunks gegen dms/internal/uploadapi hoch, resumeSessionId fragt
nach Abbruch per GET /uploads/{id} den zuletzt bestaetigten Offset ab und
setzt dort fort (Akzeptanzkriterium 3, gleicher Server-Mechanismus wie
DOC-01). lib/uploadQueue.ts ist reine, DOM-unabhaengige Reducer-Logik fuer
Status/Fortschritt je Datei (Akzeptanzkriterien 1+2) - Ids werden VOR dem
Dispatch erzeugt (createQueuedItem), nicht im Reducer selbst: ein frueher
Versuch mit einer Id-Factory-Funktion im Reducer scheiterte real beim
Testen (useReducer ruft den Reducer erst in der Render-Phase auf, Code
direkt nach dispatch() sah noch ein leeres Ids-Array).

components/Dropzone.tsx nutzt ein natives <label for>+<input type=file
multiple> fuer Tastatur-/Dateidialog-Bedienung statt eigenem
Tastatur-Handler.

14/14 Vitest-Tests (uploadQueue, chunkUpload, Dropzone, UploadManager),
mindestens ein Test je Akzeptanzkriterium. npx tsc --noEmit sauber,
npm run build erfolgreich.

Pruefungen vor der Abnahme (Details siehe DOC-03-PRUEFPROTOKOLL.md):
1. Tastatur/Dateidialog bedienbar - bestanden (Dropzone.test.tsx)
2. Netzabbruch im Browser - real gegen echten HTTP-Mock-Server + echten
   Chromium (Playwright) getestet, echter net::ERR_CONNECTION_RESET,
   Retry fuehrt zum Abschluss
3. Mobile Ansicht (375x667) - real per Playwright geprueft, kein
   horizontales Scrollen, Bedienelemente vollstaendig erreichbar

QA-03 (Pruefgate Dokumentenkern) bewusst NICHT implementiert -
completeUpload() liefert lediglich die documentId als Anknuepfungspunkt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 12:28:22 +02:00

79 lines
3.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { applyUploadQueueAction, createQueuedItem, initialQueueState, progressPercent } from "./uploadQueue";
function makeFile(name: string, size: number): File {
return new File([new Uint8Array(size)], name, { type: "application/pdf" });
}
function addFiles(state: ReturnType<typeof initialQueueState>, files: File[]) {
return applyUploadQueueAction(state, { type: "filesAdded", items: files.map(createQueuedItem) });
}
describe("uploadQueue", () => {
// Akzeptanzkriterium 1: mehrere Dateien gleichzeitig.
it("nimmt mehrere Dateien in einem Schritt auf", () => {
const files = [makeFile("a.pdf", 10), makeFile("b.pdf", 20), makeFile("c.pdf", 30)];
const state = addFiles(initialQueueState(), files);
expect(state.items).toHaveLength(3);
expect(state.items.map((i) => i.file.name)).toEqual(["a.pdf", "b.pdf", "c.pdf"]);
expect(state.items.every((i) => i.status === "queued")).toBe(true);
});
it("vergibt eindeutige IDs auch bei gleichem Dateinamen", () => {
const files = [makeFile("dupe.pdf", 10), makeFile("dupe.pdf", 10)];
const state = addFiles(initialQueueState(), files);
expect(state.items[0].id).not.toBe(state.items[1].id);
});
// Akzeptanzkriterium 2: Fortschritt und Status je Datei.
it("aktualisiert Fortschritt und Status unabhaengig je Datei", () => {
const files = [makeFile("a.pdf", 100), makeFile("b.pdf", 100)];
let state = addFiles(initialQueueState(), files);
const [idA, idB] = state.items.map((i) => i.id);
state = applyUploadQueueAction(state, { type: "uploadStarted", id: idA });
state = applyUploadQueueAction(state, { type: "progress", id: idA, bytesUploaded: 50 });
const itemA = state.items.find((i) => i.id === idA)!;
const itemB = state.items.find((i) => i.id === idB)!;
expect(itemA.status).toBe("uploading");
expect(progressPercent(itemA)).toBe(50);
expect(itemB.status).toBe("queued");
expect(progressPercent(itemB)).toBe(0);
});
it("markiert eine Datei nach Abschluss als vollstaendig (100%)", () => {
const files = [makeFile("a.pdf", 100)];
let state = addFiles(initialQueueState(), files);
const id = state.items[0].id;
state = applyUploadQueueAction(state, { type: "succeeded", id, documentId: "doc-1" });
expect(state.items[0].status).toBe("success");
expect(progressPercent(state.items[0])).toBe(100);
expect(state.items[0].documentId).toBe("doc-1");
});
// Akzeptanzkriterium 3: fehlgeschlagene Datei einzeln wiederholbar.
it("markiert eine fehlgeschlagene Datei mit Fehlermeldung, andere Dateien bleiben unberuehrt", () => {
const files = [makeFile("a.pdf", 100), makeFile("b.pdf", 100)];
let state = addFiles(initialQueueState(), files);
const [idA, idB] = state.items.map((i) => i.id);
state = applyUploadQueueAction(state, { type: "failed", id: idA, message: "Netzwerkfehler" });
const itemA = state.items.find((i) => i.id === idA)!;
const itemB = state.items.find((i) => i.id === idB)!;
expect(itemA.status).toBe("error");
expect(itemA.errorMessage).toBe("Netzwerkfehler");
expect(itemB.status).toBe("queued");
});
it("setzt eine fehlgeschlagene Datei bei retry zurueck auf queued und loescht die Fehlermeldung", () => {
const files = [makeFile("a.pdf", 100)];
let state = addFiles(initialQueueState(), files);
const id = state.items[0].id;
state = applyUploadQueueAction(state, { type: "failed", id, message: "Netzwerkfehler" });
state = applyUploadQueueAction(state, { type: "retry", id });
expect(state.items[0].status).toBe("queued");
expect(state.items[0].errorMessage).toBeUndefined();
});
});