- werkzeuge/bootstrap/: Bootstrap 5.3.8 (CSS, JS-Bundle) und Bootstrap Icons 1.13.1 (CSS, woff2/woff) byte-gleich aus den npm-Paketen, Integrität geprüft; README mit Version, Quelle, Integrität, Lizenz und Aktualisierung; Lizenzen (MIT) daneben; .gitattributes: binary, die README bleibt Text mit Diff. - werkzeuge/stil/ag_prozesse.css: eigene kleine Stilschicht (ag-*): Seite in voller Höhe, dunkle Navigationsleiste, hellgrauer Hintergrund, Karten mit farbigem Rand, Tabellenziffern, Druck. Vorbild VLN_SN/EA-Bilanz, nichts übernommen. - Vorschau: Kopfzeile als dunkle Navigationsleiste mit Icon-Knöpfen; JavaScript, IDs und Zeichenfläche (Wasserzeichen) unverändert. - scripts/vorschau.py bettet Bootstrap, Icons und Stil ein und setzt die Icon-Schrift als data:-URI ein (_schriften_einbetten); ein nicht einbettbarer Schriftverweis bricht laut ab. - Tests: drei neue in test_vorschau.py; der Aufbau von test_bibliothek_mit_script_ende_wird_abgelehnt kopiert jetzt ganz werkzeuge/ (Erwartung unverändert). - AGENTS.md: eingebettete Fremdbibliotheken sind bpmn-js und Bootstrap. - Tests: python -m pytest -q -> 256 bestanden, 1 übersprungen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
161 lines
7.6 KiB
Python
161 lines
7.6 KiB
Python
"""Tests für scripts/vorschau.py und die eingebettete bpmn-js-Vorschau – reines Python/pytest, ohne Browser."""
|
||
import json
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(ROOT / "scripts"))
|
||
|
||
import vorschau # noqa: E402
|
||
|
||
BEISPIEL_MD = ROOT / ".agents" / "skills" / "sn-bpmn" / "beispiele" / "beispiel_prozess.md"
|
||
BEISPIEL_BPMN = BEISPIEL_MD.with_suffix(".bpmn")
|
||
|
||
|
||
def test_vorlage_verweist_nur_auf_vorhandene_lokale_dateien():
|
||
"""Prüft, dass vorschau.html nur Dateien unter werkzeuge/vorschau/ einbindet, die es gibt, und nichts aus dem Netz."""
|
||
html = vorschau.VORLAGE.read_text(encoding="utf-8")
|
||
verweise = re.findall(r'(?:src|href)="([^"]+)"', html)
|
||
assert verweise, "keine eingebundenen Dateien gefunden"
|
||
for verweis in verweise:
|
||
assert not re.match(r"^[a-z]+:", verweis), f"externer Verweis: {verweis}"
|
||
assert (vorschau.VORSCHAU / verweis).is_file(), f"fehlt: {verweis}"
|
||
|
||
|
||
def test_lizenz_von_bpmn_js_liegt_bei():
|
||
"""Prüft, dass die Lizenz von bpmn-js neben der eingebetteten Bibliothek liegt (MIT-Auflage)."""
|
||
lizenz = (vorschau.VORSCHAU / "bpmn-js" / "LICENSE").read_text(encoding="utf-8")
|
||
assert "watermark" in lizenz
|
||
|
||
|
||
BOOTSTRAP = ROOT / "werkzeuge" / "bootstrap"
|
||
STIL = ROOT / "werkzeuge" / "stil" / "ag_prozesse.css"
|
||
|
||
|
||
def test_bootstrap_und_icons_liegen_mit_lizenz_bei():
|
||
"""Prüft, dass Bootstrap (CSS, JS-Bundle) und Bootstrap Icons (CSS, Schrift) lokal mit ihren MIT-Lizenzen und einer
|
||
README zu Version und Quelle eingebettet sind (Issue #27)."""
|
||
for datei in ("bootstrap.min.css", "bootstrap.bundle.min.js", "LICENSE", "README.md",
|
||
"icons/bootstrap-icons.min.css", "icons/fonts/bootstrap-icons.woff2", "icons/LICENSE"):
|
||
assert (BOOTSTRAP / datei).is_file(), f"fehlt: werkzeuge/bootstrap/{datei}"
|
||
for lizenz in ("LICENSE", "icons/LICENSE"):
|
||
assert "MIT License" in (BOOTSTRAP / lizenz).read_text(encoding="utf-8")
|
||
|
||
|
||
def test_vorlage_nutzt_bootstrap_und_den_stil_der_ag():
|
||
"""Prüft, dass die Vorschau Bootstrap, die Bootstrap Icons und das gemeinsame Stylesheet der AG einbindet
|
||
(Issue #27)."""
|
||
html = vorschau.VORLAGE.read_text(encoding="utf-8")
|
||
for verweis in ("../bootstrap/bootstrap.min.css", "../bootstrap/icons/bootstrap-icons.min.css",
|
||
"../stil/ag_prozesse.css"):
|
||
assert f'href="{verweis}"' in html, f"nicht eingebunden: {verweis}"
|
||
assert STIL.is_file()
|
||
|
||
|
||
def test_erzeugte_seite_laedt_auch_die_icon_schrift_nicht_nach():
|
||
"""Prüft, dass in der erzeugten Seite jede CSS-Adresse url(...) eine data:-Quelle oder ein Fragment im selben
|
||
Dokument ist – auch die Schrift der Bootstrap Icons, die im Original relativ auf fonts/ verweist (Issue #27)."""
|
||
html = vorschau.baue_html(BEISPIEL_BPMN.read_text(encoding="utf-8"), BEISPIEL_BPMN.name)
|
||
adressen = re.findall(r"url\(\s*['\"]?([^'\")]+)", html)
|
||
assert any(a.startswith("data:font/woff2;base64,") for a in adressen), "Icon-Schrift nicht eingebettet"
|
||
# erlaubt sind data:-Quellen und Verweise auf SVG-Fragmente im selben Dokument (url(#…), aus bpmn-js)
|
||
fremd = [a for a in adressen if not a.startswith(("data:", "#"))]
|
||
assert fremd == [], fremd[:3]
|
||
assert ".ag-nav" in html, "Stil der AG nicht eingebettet"
|
||
|
||
|
||
def test_erzeugte_seite_ist_in_sich_geschlossen():
|
||
"""Prüft, dass die erzeugte Seite Bibliothek und Stile eingebettet hat und keine Datei mehr nachlädt."""
|
||
html = vorschau.baue_html(BEISPIEL_BPMN.read_text(encoding="utf-8"), BEISPIEL_BPMN.name)
|
||
assert not re.search(r"<script[^>]*\bsrc=", html)
|
||
assert "<link" not in html
|
||
assert "BpmnJS" in html
|
||
assert "const EINGEBETTET = null;" not in html
|
||
assert '"beispiel_prozess.bpmn"' in html
|
||
|
||
|
||
def test_diagramm_wird_als_javascript_zeichenkette_eingebettet():
|
||
"""Prüft, dass der Inhalt der .bpmn verlustfrei als JSON-Zeichenkette mit maskiertem '<' in der Seite landet."""
|
||
xml = '<definitions id="x">Ä&</definitions>'
|
||
html = vorschau.baue_html(xml, "x.bpmn")
|
||
wert = re.search(r"const EINGEBETTET = (.*);", html).group(1)
|
||
assert "<" not in wert
|
||
assert json.loads(wert) == xml
|
||
|
||
|
||
def test_script_ende_im_diagramm_bricht_die_seite_nicht_auf():
|
||
"""Prüft, dass '</script>' in einer Beschriftung nicht als Ende des Skriptblocks wirkt."""
|
||
html = vorschau.baue_html('<task name="</script><script>alert(1)</script>"/>', "x.bpmn")
|
||
assert "alert(1)" in html
|
||
assert "<script>alert(1)" not in html
|
||
assert html.lower().count("</script") == html.lower().count("<script")
|
||
|
||
|
||
def test_bibliothek_mit_script_ende_wird_abgelehnt(tmp_path, monkeypatch):
|
||
"""Prüft, dass eine (etwa nach einem Update) nicht sicher einbettbare Bibliothek einen Fehler auslöst."""
|
||
shutil.copytree(vorschau.VORSCHAU.parent, tmp_path / "werkzeuge") # Vorlage braucht auch ../bootstrap, ../stil
|
||
kopie = tmp_path / "werkzeuge" / "vorschau"
|
||
js = kopie / "bpmn-js" / "bpmn-navigated-viewer.production.min.js"
|
||
js.write_text(js.read_text(encoding="utf-8") + '\nvar x = "</script>";', encoding="utf-8")
|
||
monkeypatch.setattr(vorschau, "VORSCHAU", kopie)
|
||
monkeypatch.setattr(vorschau, "VORLAGE", kopie / "vorschau.html")
|
||
with pytest.raises(RuntimeError, match="</script"):
|
||
vorschau.baue_html("<x/>")
|
||
|
||
|
||
def test_md_wird_vor_der_vorschau_umgewandelt():
|
||
"""Prüft, dass eine Arbeitsschritt-.md dasselbe XML liefert wie ihre erzeugte .bpmn."""
|
||
erwartet = BEISPIEL_BPMN.read_text(encoding="utf-8").replace("\r\n", "\n")
|
||
assert vorschau.lies_diagramm(BEISPIEL_MD).replace("\r\n", "\n") == erwartet
|
||
|
||
|
||
def test_ausgabe_in_datei_ohne_browser(tmp_path):
|
||
"""Prüft, dass --ausgabe mit --nicht-oeffnen eine Seite schreibt und Exit-Code 0 liefert."""
|
||
ziel = tmp_path / "v.html"
|
||
ergebnis = subprocess.run(
|
||
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), str(BEISPIEL_MD),
|
||
"--ausgabe", str(ziel), "--nicht-oeffnen"],
|
||
capture_output=True, text=True, encoding="utf-8",
|
||
)
|
||
assert ergebnis.returncode == 0, ergebnis.stdout + ergebnis.stderr
|
||
assert "BpmnJS" in ziel.read_text(encoding="utf-8")
|
||
|
||
|
||
def test_stdin_modus_liest_xml_und_schreibt_html():
|
||
"""Prüft, dass --stdin als Filter arbeitet (Grundlage für einen Gitea-Renderer)."""
|
||
ergebnis = subprocess.run(
|
||
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), "--stdin"],
|
||
input=BEISPIEL_BPMN.read_bytes(), capture_output=True,
|
||
)
|
||
assert ergebnis.returncode == 0, ergebnis.stderr.decode("utf-8", "replace")
|
||
html = ergebnis.stdout.decode("utf-8")
|
||
assert html.startswith("<!DOCTYPE html>") and "Antrag erfassen" in html
|
||
|
||
|
||
def test_fehlende_datei_gibt_exit_code_1(tmp_path):
|
||
"""Prüft, dass eine nicht vorhandene Eingabedatei sauber mit Exit-Code 1 endet."""
|
||
ergebnis = subprocess.run(
|
||
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), str(tmp_path / "gibt-es-nicht.bpmn"),
|
||
"--nicht-oeffnen"],
|
||
capture_output=True, text=True, encoding="utf-8",
|
||
)
|
||
assert ergebnis.returncode == 1
|
||
assert "nicht gefunden" in ergebnis.stdout
|
||
|
||
|
||
def test_nicht_umwandelbare_md_gibt_exit_code_1(tmp_path):
|
||
"""Prüft, dass eine .md mit Syntaxfehler als Meldung statt als Absturz endet."""
|
||
md = tmp_path / "kaputt.md"
|
||
md.write_text("S1 Start: A\nS1 Ende: B\n", encoding="utf-8")
|
||
ergebnis = subprocess.run(
|
||
[sys.executable, str(ROOT / "scripts" / "vorschau.py"), str(md), "--nicht-oeffnen"],
|
||
capture_output=True, text=True, encoding="utf-8",
|
||
)
|
||
assert ergebnis.returncode == 1
|
||
assert "lässt sich nicht umwandeln" in ergebnis.stdout
|