Files
Trademind/tests/test_dashboard.py
Tobias Zimmermann db31bc93f7 Konfigurationsbereich auffindbar machen
Der Bereich war sichtbar, wurde aber übersehen: ein 12-Pixel-Knopf, der an der
Überschrift klebte, während "Handel pausieren" und "Training starten" jeweils in
einer eigenen Box mit normalgroßem Knopf sitzen. 71x26 px gegenüber 90x58 und
79x58 - das liest sich nicht als Bedienelement.

Der Bereich sieht jetzt aus wie die beiden anderen: eigene Panel-Box, Knopf
"Konfiguration bearbeiten" in gleicher Größe (114x58 px, 14px Schrift), daneben
eine Zeile, die sagt, was dahintersteckt. Geöffnet heißt der Knopf
"Konfiguration schließen".

Zwei neue Tests halten die drei Haupt-Bedienelemente auf gleicher Linie: keine
Inline-Styles, die Schrift oder Polsterung verkleinern, und jeder Knopf muss in
einer Panel-Box stehen statt in einer Überschrift.

233 Tests (6 neue), ruff sauber. Im Browser durchgespielt: öffnen, 76 Felder,
risk.cooldown_bars_after_exit geändert und sofort wirksam, schließen.
2026-08-23 14:06:40 +02:00

208 lines
7.6 KiB
Python
Raw Permalink 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"
)
PRIMARY_BUTTONS = ["trade-btn", "train-btn", "cfg-toggle"]
@pytest.mark.parametrize("button_id", PRIMARY_BUTTONS)
def test_primary_buttons_are_not_shrunk_by_inline_styles(button_id: str):
"""Die Haupt-Bedienelemente sollen gleich aussehen.
Der Konfigurationsknopf war einmal ein 12-Pixel-Anhängsel an einer Überschrift und
wurde schlicht übersehen. Inline-Styles, die Schrift oder Polsterung verkleinern,
gehören hier nicht hin.
"""
match = re.search(rf'<button id="{button_id}"([^>]*)>', _DASHBOARD)
assert match is not None, f"Knopf #{button_id} fehlt"
attributes = match.group(1)
for verboten in ("font-size", "padding"):
assert verboten not in attributes, (
f"#{button_id} verkleinert sich per Inline-Style ({verboten}) gegenüber den anderen"
)
@pytest.mark.parametrize("button_id", PRIMARY_BUTTONS)
def test_primary_buttons_sit_inside_a_panel(button_id: str):
"""Ein Knopf direkt in der Überschrift liest sich nicht als Bedienelement."""
match = re.search(rf'<button id="{button_id}"[^>]*>', _DASHBOARD)
before = _DASHBOARD[: match.start()]
letzte_ueberschrift = before.rfind("<h2>")
letztes_panel = before.rfind('class="panel"')
assert letztes_panel > letzte_ueberschrift, (
f"#{button_id} steht in einer Überschrift statt in einer Panel-Box"
)
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