Die Wahl war keine Startaktion, sondern eine Einstellung, die nebenbei einen Lauf auslöste. Damit stand sie neben zwei echten Alternativen und beantwortete eine andere Frage als die gestellte. Das Intervall samt Startzeit steht im Reiter Einstellungen des Falls, dort gehört es hin und dort lässt es sich auch wieder ändern. Im Startdialog bleiben zwei Alternativen, die sich wirklich ausschließen. Den Fall aus dem Netz aufbauen oder mit eigenem Material beginnen. Fünf Prüfungen halten das fest, darunter der Nachweis, dass die automatische Aktualisierung im Einstellungen-Reiter erreichbar bleibt. Cache-Buster auf 20260802d. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
205 Zeilen
9.3 KiB
Python
205 Zeilen
9.3 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("\nB3) Der Startdialog bietet nur echte Startaktionen")
|
|
wahlen = re.findall(r"startChoice\('([a-z]+)'\)", studio_html)
|
|
pruefe("B3a genau zwei Alternativen", len(wahlen) == 2, wahlen)
|
|
pruefe("B3b aus dem Netz aufbauen oder mit eigenem Material",
|
|
sorted(wahlen) == ["full", "ingest"], sorted(wahlen))
|
|
pruefe("B3c die automatische Aktualisierung steht nicht mehr darin",
|
|
"auto" not in wahlen and "sp-auto-desc" not in studio_html)
|
|
pruefe("B3d und auch nicht mehr in der Logik",
|
|
"kind === 'auto'" not in studio_js)
|
|
pruefe("B3e sie ist aber im Einstellungen-Reiter erreichbar",
|
|
'id="set-refresh-mode"' in studio_html and 'value="auto"' in studio_html)
|
|
|
|
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)
|