Dateien
AegisSight-Monitor/tests/test_studio_struktur.py
claude-dev bd35e25121 test(studio): Zugriffe auf nicht vorhandene Elemente werden geprüft
Nachtrag aus der Endkontrolle nach dem Umbau der Bausteinspalte. Ein
getElementById auf ein entferntes Element scheitert still, genau das wäre
beim Entfernen der Artefakt-Karten leicht passiert. Die Strukturprüfung
gleicht jetzt jede gesuchte ID gegen das Dokument ab, inklusive der zur
Laufzeit erzeugten.

Für studio.js sind alle 109 Zugriffe sauber. In app.js sind fünf Verweise
ins Leere aufgefallen, alle aus der Domain-Sperrliste im Quellen-Modal und
alle hinter einer Existenzprüfung, showBlockDomainDialog kehrt bei fehlendem
Formular sofort zurück. Kein Absturzrisiko, aber toter Code. Sie stehen als
bekannte Altlast in der Prüfung, werden gemeldet und zählen nicht als
Fehler. Ein neuer Fall fällt weiterhin auf.

Dazu ein Kommentar am Label der Netzwerkanalyse, das ohne zugehörige Karte
stehen bleibt, damit später niemand rätselt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:32:17 +00:00

193 Zeilen
8.6 KiB
Python

"""Prueft die Struktur der Oberflaechen ohne Browser.
Faengt genau die Fehler, die eine Syntaxpruefung durchlaesst. Ein onclick, das
auf eine nicht vorhandene Methode zeigt. Ein Feld, das das Skript sucht, das
im Dokument aber fehlt. Eine ID, die zweimal vergeben ist. Ein vergessener
Cache-Buster.
Aufruf aus dem Projektstamm:
python3 tests/test_studio_struktur.py
"""
import os
import re
import sys
STAMM = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC = os.path.join(STAMM, "src", "static")
ok = 0
fehl = 0
def pruefe(name, bedingung, extra=None):
global ok, fehl
if bedingung:
ok += 1
print(f" OK {name}")
else:
fehl += 1
print(f" FEHL {name}" + (f" {extra}" if extra is not None else ""))
def lies(*teile):
with open(os.path.join(STATIC, *teile), encoding="utf-8") as f:
return f.read()
studio_html = lies("studio.html")
studio_js = lies("js", "studio.js")
dash_html = lies("dashboard.html")
app_js = lies("js", "app.js")
comp_js = lies("js", "components.js")
style = lies("css", "style.css")
studio_css = lies("css", "studio.css")
def ids(html):
return re.findall(r'\bid="([^"]+)"', html)
def doppelte(liste):
gesehen, doppelt = set(), []
for x in liste:
if x in gesehen:
doppelt.append(x)
gesehen.add(x)
return doppelt
print("\nA) IDs sind eindeutig")
for name, html in [("studio.html", studio_html), ("dashboard.html", dash_html)]:
d = doppelte(ids(html))
pruefe(f"A keine doppelte ID in {name}", not d, d)
print("\nB) Der Einstellungen-Reiter ist vollstaendig verdrahtet")
# Alle Felder, die die Logik anfasst, muessen im Dokument stehen.
gesucht = sorted(set(re.findall(r"_setEl\('([^']+)'\)", studio_js)))
vorhanden = set(ids(studio_html))
fehlend = [f for f in gesucht if f not in vorhanden]
pruefe("B1 jedes angesprochene Feld existiert", not fehlend, fehlend)
pruefe("B2 es sind auch wirklich welche geprueft", len(gesucht) >= 15, len(gesucht))
# Panel und Navigationsknopf haengen am selben Schluessel.
pruefe("B3 Navigationsknopf vorhanden",
'data-art="settings" onclick="Studio.openTab(\'settings\')"' in studio_html)
pruefe("B4 Panel vorhanden", '<div class="tab-panel" data-art="settings">' in studio_html)
pruefe("B5 activateTab kennt den Reiter",
"if (key === 'settings') this.loadSettings();" in studio_js)
# Bekannte tote Verweise aus der Domain-Sperrliste im Quellen-Modal. Alle
# Zugriffe liegen hinter einer Existenzpruefung (showBlockDomainDialog kehrt
# bei fehlendem Formular sofort zurueck), es droht also kein Absturz. Sie
# werden gemeldet, zaehlen aber nicht als Fehler, damit die Pruefung nicht
# dauerhaft rot steht und niemand mehr hinsieht.
ALTLASTEN = {
"block-domain-input", "block-domain-notes", "source-overview-chevron",
"sources-block-form", "sources-hint",
}
print("\nB2) Kein Zugriff auf Elemente, die es nicht gibt")
# Ein getElementById auf ein entferntes Element scheitert still. Beim Umbau
# der Bausteinspalte waere genau das leicht passiert.
for name, js_quelle, html_quelle in [("studio", studio_js, studio_html),
("dashboard", app_js, dash_html)]:
vorhanden = set(re.findall(r'\bid="([^"]+)"', html_quelle))
# Was das Skript selbst erzeugt, steht nicht im Dokument
vorhanden |= set(re.findall(r"\.id = '([^']+)'", js_quelle))
vorhanden |= set(re.findall(r'id="([^"]+)"', js_quelle))
vorhanden |= set(re.findall(r'\bid="([^"]+)"', comp_js))
gesucht = set(re.findall(r"getElementById\('([^']+)'\)", js_quelle))
fehlend = sorted(g for g in gesucht if g not in vorhanden)
neue = [g for g in fehlend if g not in ALTLASTEN]
pruefe(f"B2 {name}.js greift nur auf vorhandene Elemente zu "
f"({len(gesucht)} geprueft)", not neue, neue)
bekannt = [g for g in fehlend if g in ALTLASTEN]
if bekannt:
print(f" bekannte Altlast, abgesichert: {', '.join(bekannt)}")
print("\nC) Jedes onclick zeigt auf eine vorhandene Methode")
for name, html, js, praefix in [
("studio", studio_html, studio_js, "Studio"),
("dashboard", dash_html, app_js + comp_js, "App"),
]:
aufgerufen = sorted(set(re.findall(praefix + r"\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", html)))
fehlend = []
for m in aufgerufen:
# Methoden stehen als " name(" oder " async name(" im Objektliteral
if not re.search(r"^\s+(async\s+)?" + re.escape(m) + r"\s*\(", js, re.M):
fehlend.append(m)
pruefe(f"C {name}.html ruft nur vorhandene Methoden ({len(aufgerufen)} geprueft)",
not fehlend, fehlend)
print("\nD) Der KI-Weg ist in beiden Ansichten gleich angeboten")
for name, html, feld in [("studio.html", studio_html, "set-ai-backend"),
("studio.html anlegen", studio_html, "inc-ai-backend"),
("dashboard.html", dash_html, "inc-ai-backend")]:
block = re.search(r'<select id="' + feld + r'".*?</select>', html, re.S)
pruefe(f"D {name} bietet drei Werte an",
bool(block) and block.group(0).count("<option") == 3)
if block:
werte = re.findall(r'value="([^"]*)"', block.group(0))
pruefe(f"D {name} genau die erlaubten Werte", werte == ["", "cli", "bedrock"], werte)
print("\nE) Cache-Buster sind einheitlich")
for name, html in [("studio.html", studio_html), ("dashboard.html", dash_html)]:
stempel = set(re.findall(r"/static/(?:js|css)/[a-z0-9_.-]+\?v=([0-9a-z]+)", html))
# Fremde Bausteine (i18n, api, ws) haben eigene Staende, das ist in Ordnung.
# Geprueft wird nur, dass die in diesem Zug geaenderten Dateien mitgezogen sind.
eigene = re.findall(r"/static/(?:js|css)/(studio|app|style|components|studio)\.(?:js|css)\?v=([0-9a-z]+)", html)
pruefe(f"E {name} nennt Stempel", bool(stempel), stempel)
# Kein fester Wert. Geprueft wird, dass die in einem Zug geaenderten
# Dateien denselben Stempel tragen, sonst muesste der Test bei jedem
# Deploy mitgepflegt werden.
stempel_eigen = {v for _, v in eigene}
pruefe(f"E {name} alle eigenen Dateien tragen denselben Stempel",
len(stempel_eigen) == 1, sorted(stempel_eigen))
print("\nE2) Die Bausteinspalte hat genau einen Startpunkt")
_i = studio_html.index('id="studio-menu"')
menue = studio_html[_i:studio_html.index("</section>", _i)]
starts = re.findall(r'data-stage="([a-z]+)"', menue)
pruefe("E2a genau ein Element startet einen Lauf", len(starts) == 1, starts)
pruefe("E2b und zwar der komplette Lauf", starts == ["full"], starts)
pruefe("E2c auch im ganzen Dokument nur dieses eine",
len(re.findall(r"data-stage=", studio_html)) == 1,
len(re.findall(r"data-stage=", studio_html)))
pruefe("E2d die Ansichten starten nichts", "Studio.runStage" not in studio_html)
for tot in ["art-card", "art-run", "art-note", "run-all", "collect-btn", "art-foot"]:
pruefe(f"E2 {tot} ist restlos weg",
tot not in studio_html and tot not in studio_js and tot not in studio_css)
pruefe("E2e die Schritte tragen Ergebnisnamen",
"Artikel holen" in studio_js and "Orte erkennen" in studio_js
and "Fakten prüfen" in studio_js)
pruefe("E2f Geoparsing steht nicht mehr in der Oberflaeche",
"Geoparsing" not in studio_html)
print("\nF) Tags im neuen Panel sind ausgeglichen")
i = studio_html.index('<div class="tab-panel" data-art="settings">')
j = studio_html.index('<div class="tab-panel" data-art="export">')
teil = studio_html[i:j]
for tag in ["div", "select", "label", "textarea"]:
auf = len(re.findall(r"<" + tag + r"[\s>]", teil))
zu = len(re.findall(r"</" + tag + r">", teil))
pruefe(f"F {tag} ausgeglichen ({auf})", auf == zu, (auf, zu))
print("\nG) Das Kennzeichen nutzt vorhandene Stile")
pruefe("G1 incident-ai-badge ist definiert", ".incident-ai-badge" in style)
pruefe("G2 case-ai-tag ist definiert", ".case-ai-tag" in studio_css)
pruefe("G3 studio.html bindet style.css ein", "/static/css/style.css" in studio_html)
pruefe("G4 Kennzeichenplatz in der Kopfzeile", 'id="studio-ai-badge"' in studio_html)
print("\nH) Keine unbekannten Design-Tokens")
# var(--x, wert) ist auch ohne Definition gueltig, der Fallback greift.
# Geprueft wird deshalb nur, was ohne Fallback benutzt wird.
ohne_fallback = set(re.findall(r"var\((--[a-z0-9-]+)\s*\)", studio_css))
definiert = set(re.findall(r"^\s*(--[a-z0-9-]+)\s*:", style, re.M))
definiert |= set(re.findall(r"^\s*(--[a-z0-9-]+)\s*:", studio_css, re.M))
unbekannt = sorted(ohne_fallback - definiert)
pruefe("H1 jedes Token ohne Fallback ist definiert", not unbekannt, unbekannt)
pruefe("H2 es sind auch wirklich welche geprueft", len(ohne_fallback) >= 10, len(ohne_fallback))
print(f"\nErgebnis: {ok} bestanden, {fehl} fehlgeschlagen")
sys.exit(1 if fehl else 0)