Files
tessera-ctl/apps/web/src/components/dashboard/widgets/xframe-config.test.ts
T
schalli 445b1d3100 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>
2026-09-22 12:12:18 +02:00

204 lines
8.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
XFRAME_CROP_DEFAULT,
XFRAME_PAGE_WIDTH,
XFRAME_RELOAD_MAX,
XFRAME_RELOAD_MIN,
XFRAME_RELOAD_OPTIONS,
XFRAME_SANDBOX,
XFRAME_TITLE_MAX,
XFRAME_ZOOM_OPTIONS,
clampXframeCrop,
isHttpsUrl,
resolveXframeConfig,
} from './xframe-config';
/**
* xframe-config.test — NEU (quick-260921-qd3, XFrame).
*
* Reiner Resolver ohne React: die API prueft Config-Felder nicht — allein
* diese Funktion entscheidet, was zum `<iframe src>` wird (T-QD3-03). Dazu
* die Sandbox-Konstante (T-QD3-01: keine Top-Navigation, keine Modals) und
* die Klemmung des Neulade-Intervalls (T-QD3-08).
*/
describe('resolveXframeConfig (quick-260921-qd3)', () => {
it('Test 1: leere Konfiguration -> keine Adresse, leerer Titel, nie neu laden', () => {
expect(resolveXframeConfig({})).toEqual({
url: null,
title: '',
reloadSeconds: 0,
crop: null,
zoom: 100,
readOnly: false,
});
});
it('Test 2: https-Adresse bleibt (getrimmt)', () => {
expect(resolveXframeConfig({ url: 'https://intern.example/board' }).url).toBe('https://intern.example/board');
expect(resolveXframeConfig({ url: ' https://a.de/x ' }).url).toBe('https://a.de/x');
});
it('Test 3: http, javascript, data, ftp, Unparsbares, Zahl und leer -> null', () => {
for (const url of ['http://a.de', 'javascript:alert(1)', 'data:text/html,x', 'ftp://a.de', 'kein-url', 42, '']) {
expect(resolveXframeConfig({ url }).url, `url=${String(url)}`).toBeNull();
}
});
it('Test 4: Titel getrimmt, auf 100 Zeichen gekuerzt, Nicht-String -> leer', () => {
expect(resolveXframeConfig({ title: ' Board ' }).title).toBe('Board');
expect(resolveXframeConfig({ title: 'x'.repeat(150) }).title).toHaveLength(XFRAME_TITLE_MAX);
expect(XFRAME_TITLE_MAX).toBe(100);
expect(resolveXframeConfig({ title: 7 }).title).toBe('');
});
it('Test 5: reloadSeconds fehlt / "abc" / -5 / 30 -> 0 (unter der kleinsten Stufe heisst nie)', () => {
expect(resolveXframeConfig({}).reloadSeconds).toBe(0);
expect(resolveXframeConfig({ reloadSeconds: 'abc' }).reloadSeconds).toBe(0);
expect(resolveXframeConfig({ reloadSeconds: -5 }).reloadSeconds).toBe(0);
expect(resolveXframeConfig({ reloadSeconds: 30 }).reloadSeconds).toBe(0);
expect(resolveXframeConfig({ reloadSeconds: Number.NaN }).reloadSeconds).toBe(0);
});
it('Test 6: reloadSeconds 60 -> 60, 90 -> 60, 599 -> 300, 600 -> 600 (groesste Stufe <= n)', () => {
expect(resolveXframeConfig({ reloadSeconds: 60 }).reloadSeconds).toBe(60);
expect(resolveXframeConfig({ reloadSeconds: 90 }).reloadSeconds).toBe(60);
expect(resolveXframeConfig({ reloadSeconds: 599 }).reloadSeconds).toBe(300);
expect(resolveXframeConfig({ reloadSeconds: 600 }).reloadSeconds).toBe(600);
});
it('Test 7: reloadSeconds 99999 -> 3600 (Obergrenze)', () => {
expect(resolveXframeConfig({ reloadSeconds: 99999 }).reloadSeconds).toBe(XFRAME_RELOAD_MAX);
expect(XFRAME_RELOAD_MAX).toBe(3600);
expect(XFRAME_RELOAD_MIN).toBe(60);
});
it('Test 8: alle drei Felder zusammen', () => {
expect(
resolveXframeConfig({ url: 'https://a.de/', title: ' Board ', reloadSeconds: 1800 }),
).toEqual({
url: 'https://a.de/',
title: 'Board',
reloadSeconds: 1800,
crop: null,
zoom: 100,
readOnly: false,
});
});
});
/**
* quick-260922-ge2: Ausschnitt, Zoom, readOnly. Die Werte landen in
* Inline-Styles (T-GE2-05) — der Resolver klemmt und weist nie ab: ein
* manipulierter Wert ergibt hoechstens einen anderen Ausschnitt, nie
* `scale(NaN)` oder einen 10⁹-px-Rahmen.
*/
describe('resolveXframeConfig — Ausschnitt, Zoom, readOnly (quick-260922-ge2)', () => {
it('Test 13: crop fehlt / null / String / unvollstaendig / Nicht-Zahl -> null', () => {
expect(resolveXframeConfig({}).crop).toBeNull();
expect(resolveXframeConfig({ crop: null }).crop).toBeNull();
expect(resolveXframeConfig({ crop: 'x' }).crop).toBeNull();
expect(resolveXframeConfig({ crop: { x: 1 } }).crop).toBeNull();
expect(resolveXframeConfig({ crop: { x: 'a', y: 0, w: 500, h: 300 } }).crop).toBeNull();
expect(resolveXframeConfig({ crop: { x: Number.NaN, y: 0, w: 500, h: 300 } }).crop).toBeNull();
});
it('Test 14: gueltiger Ausschnitt bleibt unveraendert; x wird verschoben, wenn x + w > 1280 (nie abgewiesen)', () => {
expect(resolveXframeConfig({ crop: { x: 0, y: 0, w: 1280, h: 720 } }).crop).toEqual({ x: 0, y: 0, w: 1280, h: 720 });
expect(resolveXframeConfig({ crop: { x: 500, y: 0, w: 1000, h: 300 } }).crop).toEqual({ x: 280, y: 0, w: 1000, h: 300 });
});
it('Test 15: Breite und Hoehe geklemmt — w 50 -> 100, w 5000 -> 1280 (x -> 0), h 10 -> 60, h 9999 -> 4000', () => {
expect(resolveXframeConfig({ crop: { x: 0, y: 0, w: 50, h: 300 } }).crop).toEqual({ x: 0, y: 0, w: 100, h: 300 });
expect(resolveXframeConfig({ crop: { x: 300, y: 0, w: 5000, h: 300 } }).crop).toEqual({ x: 0, y: 0, w: 1280, h: 300 });
expect(resolveXframeConfig({ crop: { x: 0, y: 0, w: 500, h: 10 } }).crop).toEqual({ x: 0, y: 0, w: 500, h: 60 });
expect(resolveXframeConfig({ crop: { x: 0, y: 0, w: 500, h: 9999 } }).crop).toEqual({ x: 0, y: 0, w: 500, h: 4000 });
});
it('Test 16: negative Position -> 0; Nachkommastellen gerundet', () => {
expect(resolveXframeConfig({ crop: { x: -5, y: -1, w: 500, h: 300 } }).crop).toEqual({ x: 0, y: 0, w: 500, h: 300 });
expect(resolveXframeConfig({ crop: { x: 10.6, y: 2.4, w: 500.4, h: 300.5 } }).crop).toEqual({
x: 11,
y: 2,
w: 500,
h: 301,
});
});
it('Test 17: zoom fehlt / "abc" / NaN -> 100; 70 -> 60, 200 -> 150, 0 -> 50, 10 -> 50, 125 -> 125', () => {
expect(resolveXframeConfig({}).zoom).toBe(100);
expect(resolveXframeConfig({ zoom: 'abc' }).zoom).toBe(100);
expect(resolveXframeConfig({ zoom: Number.NaN }).zoom).toBe(100);
expect(resolveXframeConfig({ zoom: 70 }).zoom).toBe(60);
expect(resolveXframeConfig({ zoom: 200 }).zoom).toBe(150);
expect(resolveXframeConfig({ zoom: 0 }).zoom).toBe(50);
expect(resolveXframeConfig({ zoom: 10 }).zoom).toBe(50);
expect(resolveXframeConfig({ zoom: 125 }).zoom).toBe(125);
});
it('Test 18: readOnly nur bei echtem true; "true", 1, fehlt -> false', () => {
expect(resolveXframeConfig({ readOnly: true }).readOnly).toBe(true);
expect(resolveXframeConfig({ readOnly: 'true' }).readOnly).toBe(false);
expect(resolveXframeConfig({ readOnly: 1 }).readOnly).toBe(false);
expect(resolveXframeConfig({}).readOnly).toBe(false);
});
it('Test 19: Konstanten — Zoom-Stufen, Seitenbreite 1280, Vorgabe-Ausschnitt 0/0/1280/720', () => {
expect(XFRAME_ZOOM_OPTIONS).toEqual([50, 60, 75, 90, 100, 125, 150]);
expect(XFRAME_PAGE_WIDTH).toBe(1280);
expect(XFRAME_CROP_DEFAULT).toEqual({ x: 0, y: 0, w: 1280, h: 720 });
});
it('Test 20: alle sechs Felder zusammen', () => {
expect(
resolveXframeConfig({
url: 'https://a.de/',
title: 'Board',
reloadSeconds: 60,
crop: { x: 100, y: 200, w: 800, h: 400 },
zoom: 75,
readOnly: true,
}),
).toEqual({
url: 'https://a.de/',
title: 'Board',
reloadSeconds: 60,
crop: { x: 100, y: 200, w: 800, h: 400 },
zoom: 75,
readOnly: true,
});
});
it('Test 21: clampXframeCrop ist exportiert — { 1200, 0, 200, 100 } -> x 1080', () => {
expect(clampXframeCrop({ x: 1200, y: 0, w: 200, h: 100 })).toEqual({ x: 1080, y: 0, w: 200, h: 100 });
});
});
describe('XFRAME_SANDBOX / XFRAME_RELOAD_OPTIONS / isHttpsUrl (quick-260921-qd3)', () => {
it('Test 9: Sandbox enthaelt scripts, same-origin, forms, popups, popups-to-escape-sandbox', () => {
const tokens = XFRAME_SANDBOX.split(' ');
for (const token of [
'allow-scripts',
'allow-same-origin',
'allow-forms',
'allow-popups',
'allow-popups-to-escape-sandbox',
]) {
expect(tokens).toContain(token);
}
});
it('Test 10: Sandbox enthaelt KEIN top-navigation und KEIN modals (T-QD3-01)', () => {
expect(XFRAME_SANDBOX).not.toMatch(/top-navigation/);
expect(XFRAME_SANDBOX).not.toMatch(/modals/);
});
it('Test 11: Auswahlstufen des Neuladens sind 0, 60, 300, 600, 1800, 3600', () => {
expect(XFRAME_RELOAD_OPTIONS).toEqual([0, 60, 300, 600, 1800, 3600]);
});
it('Test 12: isHttpsUrl ist re-exportiert — HTTPS://A.DE true, http://a.de false', () => {
expect(isHttpsUrl('HTTPS://A.DE')).toBe(true);
expect(isHttpsUrl('http://a.de')).toBe(false);
});
});