Files
Projektwissen/tests/test_vorschau.py
T
DanielandClaude Opus 5.5 b341a658bc Bootstrap lokal einbetten und Stil der AG anlegen (#27)
- 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>
2026-09-25 20:11:29 +02:00

161 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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">Ä&amp;</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