BPMN-Vorschau im Browser, offline, auf Basis von bpmn-js

- werkzeuge/vorschau/vorschau.html: Anzeige per Drag & Drop oder
  Dateiauswahl, Zoom, Einpassen, Fehlermeldungen; funktioniert ohne
  Python direkt aus dem Dateisystem.
- scripts/vorschau.py: erzeugt aus .bpmn oder Arbeitsschritt-.md eine
  in sich geschlossene Seite und öffnet sie; --stdin als Filter, etwa
  für einen Gitea-Renderer. Diagramm wird mit maskiertem '<' eingebettet,
  die Bibliothek unverändert und nur auf <script/</script geprüft.
- bpmn-js 18.29.1 (navigated viewer + CSS + LICENSE) lokal eingebettet,
  npm-Integrität geprüft; lädt nichts aus dem Netz. Wasserzeichen bleibt
  laut Lizenzauflage sichtbar.
- tests/test_vorschau.py: 11 Tests; Skill sn-bpmn, Anleitung, AGENTS.md,
  README, Leitfäden, Fallstricke und STATUS ergänzt.
- Tests: python -m pytest -q -> 55 bestanden. Darstellung mit Edge
  headless geprüft (beide Beispiele, leere Seite, kaputte Datei).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 13:32:14 +02:00
co-authored by Claude Opus 5.5
parent f582effe16
commit 19947a845f
18 changed files with 1977 additions and 7 deletions
+124
View File
@@ -0,0 +1,124 @@
"""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
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."""
kopie = tmp_path / "vorschau"
shutil.copytree(vorschau.VORSCHAU, kopie)
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