feat(quick-260922-ge2): XFrame-Widget - Ausschnitt der Seite waehlen und einpassen, Zoom fuer die ganze Seite, Nur anzeigen

- Resolver: crop (x/y/w/h, geklemmt auf 1280er-Seite, x verschoben statt abgewiesen), zoom (50..150, groesste Stufe <= n), readOnly (nur echtes true)
- xframe-crop.ts: reine Geometrie - computeCropLayout (contain + zentriert, scale 0 bis gemessen), applyCropDrag (Verschieben, vier Ecken, Gegenecke bleibt)
- Kachel: Clip + verschobener, skalierter <iframe> bei fester Layoutbreite 1280, ResizeObserver am Koerper; Zoom-Zweig mit Prozentmassen; 100 % wie bisher; readOnly-Flaeche nur im Ansichtsmodus
- Formular: Checkbox Ausschnitt (ein Aufruf mit crop + readOnly), Vorschau 1280 px breit mit derselben Sandbox und pointer-events none, Rahmen mit vier Griffen (Pointer-Events, Capture-Waechter fuer jsdom), Zahlenfelder, Zoom-Auswahl nur ohne Ausschnitt, Nur anzeigen, dauerhafter Hinweis
- Rahmen als <fieldset> statt div role=group (Biome useSemanticElements, kein biome-ignore)
- Test-Helfer stubResizeObserver, 13 neue Schluessel de/en, Ausschnitt auf der Umlaut-Allowlist; Web-Tests 604 -> 640

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 12:11:24 +02:00
parent 5aa577a7fa
commit 445b1d3100
12 changed files with 1110 additions and 31 deletions
@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest';
import { XFRAME_PREVIEW_PAGE_HEIGHT, applyCropDrag, computeCropLayout } from './xframe-crop';
/**
* xframe-crop.test — NEU (quick-260922-ge2).
*
* Reine Geometrie ohne React: Einpassen (contain) und Zentrieren des
* Ausschnitts in der Kachel, Layouthoehe des Rahmens, Nullkachel ohne
* Division durch 0 (T-GE2-05); Ziehmathematik der Vorschau — Verschieben
* bei fester Groesse, Ecken bewegen nur ihre zwei Kanten, die Gegenecke
* bleibt stehen, Mindestgroesse an der bewegten Kante.
*/
const FULL = { x: 0, y: 0, w: 1280, h: 720 };
const START = { x: 100, y: 100, w: 400, h: 300 };
describe('computeCropLayout (quick-260922-ge2)', () => {
it('Test 1: breitenbegrenzt — 1280x720 in 640x720 -> scale 0.5, left 0, top 180', () => {
expect(computeCropLayout(FULL, 640, 720)).toEqual({ scale: 0.5, left: 0, top: 180, frameHeight: 720 });
});
it('Test 2: hoehenbegrenzt — 1280x720 in 1280x180 -> scale 0.25, left 480, top 0', () => {
expect(computeCropLayout(FULL, 1280, 180)).toEqual({ scale: 0.25, left: 480, top: 0, frameHeight: 720 });
});
it('Test 3: Vergroesserung erlaubt — 200x100 in 800x400 -> scale 4', () => {
expect(computeCropLayout({ x: 0, y: 0, w: 200, h: 100 }, 800, 400)).toEqual({
scale: 4,
left: 0,
top: 0,
frameHeight: 720,
});
});
it('Test 4: frameHeight mindestens 720, sonst y + h', () => {
expect(computeCropLayout({ x: 0, y: 0, w: 1280, h: 100 }, 640, 360).frameHeight).toBe(720);
expect(computeCropLayout({ x: 0, y: 3000, w: 1280, h: 400 }, 640, 360).frameHeight).toBe(3400);
});
it('Test 5: ungemessene Kachel (0x0 oder 640x0) -> scale 0, left 0, top 0 (nichts rendern)', () => {
expect(computeCropLayout(FULL, 0, 0)).toEqual({ scale: 0, left: 0, top: 0, frameHeight: 720 });
expect(computeCropLayout(FULL, 640, 0)).toEqual({ scale: 0, left: 0, top: 0, frameHeight: 720 });
});
});
describe('applyCropDrag (quick-260922-ge2)', () => {
it('Test 6: move um +100/+50 verschiebt bei fester Groesse', () => {
expect(applyCropDrag('move', START, 100, 50)).toEqual({ x: 200, y: 150, w: 400, h: 300 });
});
it('Test 7: move ueber den Rand -> an der Kante geklemmt (x 880 = 1280 - 400; -500/-500 -> 0/0)', () => {
expect(applyCropDrag('move', START, 2000, 0)).toEqual({ x: 880, y: 100, w: 400, h: 300 });
expect(applyCropDrag('move', START, -500, -500)).toEqual({ x: 0, y: 0, w: 400, h: 300 });
});
it('Test 8: se um +100/+100 -> groesser, obere linke Ecke bleibt', () => {
expect(applyCropDrag('se', START, 100, 100)).toEqual({ x: 100, y: 100, w: 500, h: 400 });
});
it('Test 9: nw um +350/+10 -> Mindestbreite 100 an der bewegten Kante, rechte Kante 500 bleibt', () => {
expect(applyCropDrag('nw', START, 350, 10)).toEqual({ x: 400, y: 110, w: 100, h: 290 });
});
it('Test 10: ne um +2000/0 -> rechte Kante 1280 (w 1180), linke Kante bleibt', () => {
expect(applyCropDrag('ne', START, 2000, 0)).toEqual({ x: 100, y: 100, w: 1180, h: 300 });
});
it('Test 11: sw um 0/+5000 -> untere Kante an der Vorschauhoehe 3000 (h 2900)', () => {
expect(XFRAME_PREVIEW_PAGE_HEIGHT).toBe(3000);
expect(applyCropDrag('sw', START, 0, 5000)).toEqual({ x: 100, y: 100, w: 400, h: 2900 });
});
});