4551339632
Das Dashboard blieb bei "lädt ..." stehen und zeigte weder Kacheln noch die
Steuerungs-Abschnitte.
Ursache: _DASHBOARD ist ein Python-String. Der in d081edf ergänzte
prompt()-Dialog für die Live-Bestätigung enthält \n, das Python zu echten
Zeilenumbrüchen aufgelöst hat. Das dadurch offene JavaScript-String-Literal ist
ein Syntaxfehler, der den gesamten <script>-Block mitreißt - im Browser waren
refresh und $ schlicht undefined, es lief kein einziger Ausdruck. Behoben durch
ein r-Präfix am Template, mit Kommentar an Ort und Stelle.
Warum das durchgerutscht ist: Die bisherige Prüfung hat nur kontrolliert, ob die
Bedienelemente im HTML stehen und ob die HTTP-Endpunkte antworten. Beides war
grün, während die Seite tot war. Markup-Präsenz ersetzt nicht das Ausführen der
Seite.
Neu: tests/test_dashboard.py
- Mini-Lexer über das ausgelieferte Skript, der String-Literale findet, die vor
dem Zeilenende nicht geschlossen werden (Backtick-Template-Literale dürfen
mehrzeilig sein). Gegenprobe im Test enthalten; gegen den echten Defekt
geprüft, er wird erkannt.
- Ausgeglichene Klammern, jede im Skript referenzierte Element-ID existiert im
HTML, und \n kommt als zwei Zeichen beim Browser an.
186 Tests (12 neue), ruff sauber. Im Browser durchgeklickt: Skript lädt, 10
Kacheln, Handel pausieren/starten und der Lernschalter ändern den Serverzustand
und die Anzeige, historisches Training läuft durch bis "Fertig in 0.8s: +146
Beobachtungen, gespeichert", null Loop-Fehler.
123 lines
4.2 KiB
Python
123 lines
4.2 KiB
Python
r"""Das eingebettete Dashboard-Skript muss syntaktisch heil beim Browser ankommen.
|
||
|
||
Hintergrund: ``_DASHBOARD`` ist ein Python-String. Fehlt das r-Präfix, macht Python aus
|
||
einem ``\n``, das eigentlich dem JavaScript gehört, einen echten Zeilenumbruch. Das offene
|
||
String-Literal reißt dann das gesamte ``<script>`` mit – die Seite lädt, zeigt aber nur
|
||
"lädt …", weil kein einziger Ausdruck ausgeführt wird. Genau das ist schon passiert.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
import pytest
|
||
|
||
from trademind.server import _DASHBOARD
|
||
|
||
SCRIPT_RE = re.compile(r"<script>(.*?)</script>", re.DOTALL)
|
||
|
||
|
||
def script_body() -> str:
|
||
match = SCRIPT_RE.search(_DASHBOARD)
|
||
assert match is not None, "Das Dashboard enthält keinen <script>-Block"
|
||
return match.group(1)
|
||
|
||
|
||
def unterminated_string(js: str) -> str | None:
|
||
"""Findet ein String-Literal, das vor dem Zeilenende nicht geschlossen wird.
|
||
|
||
Nur Template-Literale (Backticks) dürfen über mehrere Zeilen gehen. Gibt eine
|
||
Beschreibung der Fundstelle zurück oder ``None``, wenn alles sauber ist.
|
||
"""
|
||
state: str | None = None # None | " | ' | ` | // | /*
|
||
opened_at = 0
|
||
line = 1
|
||
i = 0
|
||
while i < len(js):
|
||
char = js[i]
|
||
nxt = js[i + 1 : i + 2]
|
||
if state is None:
|
||
if char == "/" and nxt == "/":
|
||
state, i = "//", i + 1
|
||
elif char == "/" and nxt == "*":
|
||
state, i = "/*", i + 1
|
||
elif char in "\"'`":
|
||
state, opened_at = char, line
|
||
elif char == "\n":
|
||
line += 1
|
||
elif state == "//":
|
||
if char == "\n":
|
||
state, line = None, line + 1
|
||
elif state == "/*":
|
||
if char == "*" and nxt == "/":
|
||
state, i = None, i + 1
|
||
elif char == "\n":
|
||
line += 1
|
||
else: # innerhalb eines String-Literals
|
||
if char == "\\":
|
||
i += 1 # nächstes Zeichen ist escaped
|
||
elif char == state:
|
||
state = None
|
||
elif char == "\n":
|
||
if state != "`":
|
||
return f"String mit {state} in Zeile {opened_at} endet nicht auf derselben Zeile"
|
||
line += 1
|
||
i += 1
|
||
|
||
if state in ("\"", "'", "`"):
|
||
return f"String mit {state} ab Zeile {opened_at} wird nie geschlossen"
|
||
return None
|
||
|
||
|
||
def test_script_has_no_unterminated_string_literal():
|
||
assert unterminated_string(script_body()) is None
|
||
|
||
|
||
def test_javascript_escapes_survive_python():
|
||
r"""Ein \n im Quelltext muss als zwei Zeichen beim Browser ankommen."""
|
||
assert "\\n" in _DASHBOARD, "Escape-Sequenz wurde von Python aufgelöst – r-String fehlt?"
|
||
|
||
|
||
def test_braces_are_balanced():
|
||
js = script_body()
|
||
depth = 0
|
||
for char in js:
|
||
depth += char == "{"
|
||
depth -= char == "}"
|
||
assert depth >= 0, "Mehr schließende als öffnende Klammern"
|
||
assert depth == 0, f"Klammern unausgeglichen (Differenz {depth})"
|
||
|
||
|
||
def test_every_referenced_element_exists():
|
||
"""Die Skript-Initialisierung greift auf feste IDs zu – fehlt eine, bricht alles ab."""
|
||
js = script_body()
|
||
referenced = set(re.findall(r"""\$\(["']([\w-]+)["']\)""", js))
|
||
referenced |= set(re.findall(r"""getElementById\(["']([\w-]+)["']\)""", js))
|
||
assert referenced, "Keine Element-Referenzen gefunden – Test greift ins Leere"
|
||
for element_id in sorted(referenced):
|
||
assert f'id="{element_id}"' in _DASHBOARD, f"Element #{element_id} fehlt im HTML"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"needle",
|
||
[
|
||
"Automatisierter Handel",
|
||
"Training starten",
|
||
"Kontinuierliches Lernen",
|
||
"control/trading",
|
||
"control/train/history",
|
||
"control/train/live",
|
||
"X-TradeMind-Token",
|
||
],
|
||
)
|
||
def test_dashboard_wires_up_the_controls(needle: str):
|
||
assert needle in _DASHBOARD
|
||
|
||
|
||
def test_broken_example_is_detected():
|
||
"""Gegenprobe: Der Prüfer muss den ursprünglichen Fehler erkennen."""
|
||
broken = 'const a = prompt("erste Zeile\nzweite Zeile");'
|
||
assert unterminated_string(broken) is not None
|
||
assert unterminated_string('const a = prompt("erste Zeile\\nzweite Zeile");') is None
|
||
assert unterminated_string("const a = `mehrere\nZeilen sind hier erlaubt`;") is None
|