internal/documentapi: neue Backend-Voraussetzung, die DOC-01/DOC-04 noch
nicht hatten - GET /documents/{id}/content liefert den entschluesselten
Originalinhalt der aktuellen Revision, Content-Type ausschliesslich ueber
Magic-Bytes, Content-Disposition: inline (kein Download-Zwang), volle
HTTP-Range-Unterstuetzung ueber http.ServeContent (Standardbibliothek) -
darueber laedt PDF.js grosse Dokumente automatisch seitenweise nach.
web/pdf-viewer: Next.js/React/TypeScript, PDF.js ausschliesslich auf
Canvas gerendert (kein iframe, siehe "Bekannte Fehler vermeiden"). Worker
laeuft aus public/pdf.worker.min.mjs (npm run copy-pdf-worker, Teil von
predev/prebuild) - bewusst NICHT ueber ein externes CDN, damit die
Anwendung vollstaendig offline/lokal lauffaehig bleibt. lib/viewerState.ts
kapselt Seiten-/Zoom-Navigation und Tastaturbedienung als reine,
DOM-unabhaengige Funktionen (Akzeptanzkriterium 2), Suche nutzt PDF.js'
eigene Textextraktion.
Pruefungen:
1. 300-seitiges PDF oeffnet im Zeitbudget - echter pdfjs-dist-Ladetest
gegen den echten Backend-Dienst: 247ms (Budget 5000ms)
2. Viewer per Tastatur bedienbar - 15 Vitest-Tests fuer Pfeiltasten/Zoom/
Suche-Fokus, inkl. Ignorieren waehrend im Suchfeld getippt wird
3. Speicherverbrauch im Browser - OFFEN, kein Headless-Browser in dieser
Umgebung verfuegbar (gleiche Einschraenkung wie bei ANA-02)
SHL-01 (Core-Board, eigener Branch-Strang mit gemeinsamer Basis) sauber
gemergt - kein Dateiueberschneidungs-Konflikt (web/shl vs. dms/).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
MAX_ZOOM,
|
|
MIN_ZOOM,
|
|
applyViewerAction,
|
|
goToPage,
|
|
initialViewerState,
|
|
mapKeyToAction,
|
|
nextPage,
|
|
previousPage,
|
|
zoomIn,
|
|
zoomOut,
|
|
} from "./viewerState";
|
|
|
|
describe("Seitennavigation (Akzeptanzkriterium 2: Blättern)", () => {
|
|
it("nextPage erhöht die aktuelle Seite", () => {
|
|
const state = initialViewerState(10);
|
|
expect(nextPage(state).currentPage).toBe(2);
|
|
});
|
|
|
|
it("nextPage bleibt auf der letzten Seite stehen", () => {
|
|
const state = goToPage(initialViewerState(3), 3);
|
|
expect(nextPage(state).currentPage).toBe(3);
|
|
});
|
|
|
|
it("previousPage bleibt auf Seite 1 stehen, geht nicht unter 1", () => {
|
|
const state = initialViewerState(10);
|
|
expect(previousPage(state).currentPage).toBe(1);
|
|
});
|
|
|
|
it("goToPage begrenzt auf den gültigen Seitenbereich", () => {
|
|
const state = initialViewerState(50);
|
|
expect(goToPage(state, 999).currentPage).toBe(50);
|
|
expect(goToPage(state, -5).currentPage).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("Zoom (Akzeptanzkriterium 2: Zoom)", () => {
|
|
it("zoomIn erhöht den Zoomfaktor um den Schritt", () => {
|
|
const state = initialViewerState(1);
|
|
expect(zoomIn(state).zoom).toBeCloseTo(1.25);
|
|
});
|
|
|
|
it("zoomOut verringert den Zoomfaktor", () => {
|
|
const state = initialViewerState(1);
|
|
expect(zoomOut(state).zoom).toBeCloseTo(0.75);
|
|
});
|
|
|
|
it("Zoom bleibt innerhalb von MIN_ZOOM und MAX_ZOOM", () => {
|
|
let state = initialViewerState(1);
|
|
for (let i = 0; i < 20; i++) state = zoomOut(state);
|
|
expect(state.zoom).toBe(MIN_ZOOM);
|
|
|
|
state = initialViewerState(1);
|
|
for (let i = 0; i < 20; i++) state = zoomIn(state);
|
|
expect(state.zoom).toBe(MAX_ZOOM);
|
|
});
|
|
});
|
|
|
|
describe("Tastaturbedienung (Akzeptanzkriterium 2 + Pflichtprüfung: Viewer per Tastatur bedienbar)", () => {
|
|
it("Pfeiltasten steuern Seiten vor/zurück", () => {
|
|
expect(mapKeyToAction("ArrowRight", false)).toEqual({ type: "nextPage" });
|
|
expect(mapKeyToAction("ArrowLeft", false)).toEqual({ type: "previousPage" });
|
|
});
|
|
|
|
it("+/- steuern Zoom", () => {
|
|
expect(mapKeyToAction("+", false)).toEqual({ type: "zoomIn" });
|
|
expect(mapKeyToAction("-", false)).toEqual({ type: "zoomOut" });
|
|
});
|
|
|
|
it("/ fokussiert die Suche", () => {
|
|
expect(mapKeyToAction("/", false)).toEqual({ type: "focusSearch" });
|
|
});
|
|
|
|
it("ignoriert Tastenkürzel, während im Suchfeld getippt wird", () => {
|
|
expect(mapKeyToAction("ArrowRight", true)).toBeNull();
|
|
expect(mapKeyToAction("+", true)).toBeNull();
|
|
});
|
|
|
|
it("unbekannte Tasten liefern keine Aktion", () => {
|
|
expect(mapKeyToAction("F5", false)).toBeNull();
|
|
});
|
|
|
|
it("applyViewerAction wendet die gemappte Aktion korrekt auf den Zustand an", () => {
|
|
const state = initialViewerState(5);
|
|
const action = mapKeyToAction("ArrowRight", false)!;
|
|
expect(applyViewerAction(state, action).currentPage).toBe(2);
|
|
});
|
|
});
|