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>
79 lines
3.6 KiB
TypeScript
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();
|
|
});
|
|
});
|