Files
Trademind/tests/test_dashboard.py
T
Tobias Zimmermann 17fef9abe5 Konfigurationsbereich im Dashboard sichtbar machen
Der Abschnitt war für Benutzer unerreichbar: Er startet mit hidden, und sichtbar
gemacht hat ihn nur renderConfig() - die ausschließlich nach einem Klick auf
"anzeigen" lief, einen Knopf innerhalb des versteckten Abschnitts. Er konnte sich
also nie selbst einblenden. refresh() rief renderConfig nicht auf.

Die Sichtbarkeit entscheidet jetzt der Statuslauf, der ohnehin alle fünf Sekunden
durchläuft, anhand von trading.control_enabled.

Warum das durchgerutscht ist: Die Prüfung des Formulars lief über
getElementById("cfg-toggle").click(). Das funktioniert auch bei unsichtbaren
Elementen - die gesamte Konfigurationsoberfläche wurde damit über einen Knopf
bedient, den kein Mensch je gesehen hätte. Dieselbe Lücke wie beim vorigen
Dashboard-Fehler: Mechanik geprüft, Sichtbarkeit nicht.

Neuer Test: Für jeden <section ... hidden> wird geprüft, dass er im Statuslauf
sichtbar gemacht wird, also in refresh() oder einer der von dort gerufenen
render-Funktionen. Gegenprobe gelaufen - vor dem Fix meldet er config-section als
unerreichbar, nach dem Fix sind alle drei Abschnitte in Ordnung.

227 Tests (5 neue), ruff sauber. Im Browser ohne jeden Skriptklick geprüft: Der
Abschnitt erscheint nach dem Laden, der Knopf liegt mit 71x26 px im Dokumentfluss,
keine Blockade durch display, visibility, pointer-events oder opacity in der
Elternkette, und das Panel öffnet mit 76 Eingabefeldern.
2026-08-23 13:54:07 +02:00

176 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
def function_body(js: str, name: str) -> str:
"""Rumpf einer Funktion per Klammerzählung ausschneiden."""
start = js.index(f"function {name}(")
open_brace = js.index("{", start)
depth = 0
for i in range(open_brace, len(js)):
depth += js[i] == "{"
depth -= js[i] == "}"
if depth == 0:
return js[open_brace : i + 1]
raise AssertionError(f"Funktion {name} ist nicht geschlossen")
def hidden_section_ids() -> list[str]:
return re.findall(r'<section id="([\w-]+)"[^>]*\shidden', _DASHBOARD)
def status_loop_code() -> str:
"""Der Code, der bei jedem Statuslauf durchläuft: refresh und die daraus gerufenen
render-Funktionen. Nur was hier steht, kann einen Abschnitt von selbst einblenden."""
js = script_body()
body = function_body(js, "refresh")
code = [body]
for name in sorted(set(re.findall(r"\b(render\w+)\(", body))):
code.append(function_body(js, name))
return "\n".join(code)
def test_there_are_hidden_sections_to_check():
assert hidden_section_ids(), "Test greift ins Leere keine versteckten Abschnitte gefunden"
@pytest.mark.parametrize("section_id", hidden_section_ids())
def test_hidden_sections_are_unhidden_by_the_status_loop(section_id: str):
"""Ein Abschnitt, dessen Aufklapp-Knopf in ihm selbst sitzt, blendet sich sonst nie ein.
Genau das ist passiert: Der Konfigurationsbereich war für Benutzer unerreichbar, weil
nur ein Klick darin ihn sichtbar gemacht hätte. Sichtbarkeit gehört deshalb in
``refresh()``, das bei jedem Statuslauf durchläuft.
"""
code = status_loop_code()
assert f'$("{section_id}").hidden' in code, (
f"#{section_id} startet versteckt und wird im Statuslauf nicht sichtbar gemacht "
"ein Knopf innerhalb des Abschnitts kann ihn nicht einblenden"
)
def test_render_functions_are_called_by_the_status_loop():
body = function_body(script_body(), "refresh")
for name in ("renderTrading", "renderTraining"):
assert f"{name}(" in body, f"{name} wird nie aufgerufen"
@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