feat(quick-260921-qd3): XFrame-Widget - Webseite als Rahmen im Dashboard, Sandbox ohne Top-Navigation, Neuladen-Intervall
- xframe-config.ts: Resolver (https-Pruefung via isHttpsUrl des Bilderrahmens, Titel bis 100 Zeichen, Neuladen 0/60/300/600/1800/3600 s geklemmt), XFRAME_SANDBOX ohne allow-top-navigation und allow-modals; 12 Tests zuerst rot - xframe-widget.tsx: genau ein <iframe> (sandbox, allow="", no-referrer, lazy), Kopfleiste mit Titel oder Ecksymbol "In neuem Tab oeffnen", Neuladen ueber key-Wechsel mit Timer-Raeumung, transparente Flaeche im Bearbeitungsmodus damit die Kachel Ziehgriff bleibt; 12 Tests zuerst rot - xframe-config-form.tsx: Adresse/Titel mit Uebernahme bei Blur/Enter, http wird mit Meldung abgewiesen und nicht gespeichert, Intervall-Auswahl, dauerhafter Hinweis auf verweigertes Einbetten; 8 Tests - Panel-Zweig samt "— Titel" in der Kopfzeile, Registry (12x12, Fenster-Symbol), Katalog, Seite, DTO @IsIn, de/en widgets.xframe (15 Schluessel), Umlaut-Allowlist "neuem"; der Server ruft die Adresse nie ab Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
XFRAME_RELOAD_MAX,
|
||||
XFRAME_RELOAD_MIN,
|
||||
XFRAME_RELOAD_OPTIONS,
|
||||
XFRAME_SANDBOX,
|
||||
XFRAME_TITLE_MAX,
|
||||
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 });
|
||||
});
|
||||
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user