445b1d3100
- 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>
204 lines
8.5 KiB
TypeScript
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);
|
|
});
|
|
});
|