Dashboard reparieren: Escape-Sequenz zerlegte das eingebettete Skript
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.
This commit is contained in:
@@ -31,7 +31,10 @@ class Controller(Protocol):
|
||||
|
||||
def trading_control_status(self) -> dict[str, Any]: ...
|
||||
|
||||
_DASHBOARD = """<!doctype html>
|
||||
# Roh-String: Escape-Sequenzen wie \n gehören dem eingebetteten JavaScript, nicht Python.
|
||||
# Ohne das r wird aus \n ein echter Zeilenumbruch mitten im JS-String-Literal – das Skript
|
||||
# lässt sich dann nicht mehr parsen und das Dashboard bleibt bei "lädt …" stehen.
|
||||
_DASHBOARD = r"""<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>TradeMind</title>
|
||||
<style>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
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
|
||||
Reference in New Issue
Block a user