A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
255 lines
10 KiB
Python
255 lines
10 KiB
Python
"""
|
|
Tests fuer das BreakPilot Studio Frontend (studio.py)
|
|
|
|
Testet CSS-Regeln und HTML-Struktur des Frontends.
|
|
|
|
Nach dem Refactoring (2024-12-16) werden CSS und JS aus separaten Dateien geladen:
|
|
- CSS: frontend/static/css/studio.css
|
|
- JS: frontend/static/js/studio.js
|
|
- HTML: frontend/templates/studio.html
|
|
"""
|
|
import pytest
|
|
import re
|
|
import sys
|
|
import os
|
|
from pathlib import Path
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
# Pfade zu den statischen Dateien
|
|
FRONTEND_DIR = Path(__file__).parent.parent / "frontend"
|
|
STATIC_CSS = FRONTEND_DIR / "static" / "css" / "studio.css"
|
|
STATIC_JS = FRONTEND_DIR / "static" / "js" / "studio.js"
|
|
TEMPLATE_HTML = FRONTEND_DIR / "templates" / "studio.html"
|
|
|
|
|
|
class TestStudioRefactoringStructure:
|
|
"""Tests fuer die refactored Dateistruktur."""
|
|
|
|
def test_css_file_exists(self):
|
|
"""Testet, dass die CSS-Datei existiert."""
|
|
assert STATIC_CSS.exists(), f"CSS-Datei nicht gefunden: {STATIC_CSS}"
|
|
|
|
def test_js_file_exists(self):
|
|
"""Testet, dass die JS-Datei existiert."""
|
|
assert STATIC_JS.exists(), f"JS-Datei nicht gefunden: {STATIC_JS}"
|
|
|
|
def test_html_template_exists(self):
|
|
"""Testet, dass das HTML-Template existiert."""
|
|
assert TEMPLATE_HTML.exists(), f"HTML-Template nicht gefunden: {TEMPLATE_HTML}"
|
|
|
|
def test_html_references_css(self):
|
|
"""Testet, dass das HTML-Template die CSS-Datei referenziert."""
|
|
html_content = TEMPLATE_HTML.read_text(encoding="utf-8")
|
|
assert '/static/css/studio.css' in html_content, \
|
|
"HTML-Template muss CSS-Datei referenzieren"
|
|
|
|
def test_html_references_js(self):
|
|
"""Testet, dass das HTML-Template die JS-Datei referenziert."""
|
|
html_content = TEMPLATE_HTML.read_text(encoding="utf-8")
|
|
assert '/static/js/studio.js' in html_content, \
|
|
"HTML-Template muss JS-Datei referenzieren"
|
|
|
|
def test_studio_py_loads_template(self):
|
|
"""Testet, dass studio.py das Template laedt."""
|
|
from frontend.studio import app_ui
|
|
html = app_ui()
|
|
# Nach Refactoring sollte HTML aus Template kommen
|
|
assert '<!DOCTYPE html>' in html or 'BreakPilot' in html, \
|
|
"studio.py muss HTML-Inhalt zurueckgeben"
|
|
|
|
|
|
class TestStudioSidebarCSS:
|
|
"""Tests fuer die Sidebar CSS-Eigenschaften."""
|
|
|
|
@pytest.fixture
|
|
def studio_css(self):
|
|
"""Laedt den CSS-Inhalt aus der separaten CSS-Datei.
|
|
|
|
Nach dem CSS-Refactoring sind die Styles modularisiert.
|
|
Sidebar-Styles befinden sich in modules/admin/sidebar.css.
|
|
"""
|
|
# Primaer: Modularisierte Sidebar-Datei
|
|
sidebar_css_path = FRONTEND_DIR / "static" / "css" / "modules" / "admin" / "sidebar.css"
|
|
if sidebar_css_path.exists():
|
|
return sidebar_css_path.read_text(encoding="utf-8")
|
|
|
|
# Fallback: Legacy studio.css
|
|
return STATIC_CSS.read_text(encoding="utf-8")
|
|
|
|
@pytest.fixture
|
|
def studio_html(self):
|
|
"""Laedt den HTML-Inhalt aus dem Template."""
|
|
return TEMPLATE_HTML.read_text(encoding="utf-8")
|
|
|
|
def test_sidebar_has_overflow_y_auto(self, studio_css):
|
|
"""
|
|
Testet, dass die Sidebar vertikal scrollbar ist.
|
|
|
|
Regression Test: Die Sidebar muss overflow-y: auto haben,
|
|
damit GPU Start/Stop Buttons sichtbar sind wenn viele
|
|
Kostenzeilen vorhanden sind.
|
|
"""
|
|
# Suche nach .sidebar CSS-Block
|
|
sidebar_css_pattern = r'\.sidebar\s*\{[^}]*\}'
|
|
sidebar_css_match = re.search(sidebar_css_pattern, studio_css, re.DOTALL)
|
|
|
|
assert sidebar_css_match is not None, "Sidebar CSS-Block nicht gefunden"
|
|
|
|
sidebar_css = sidebar_css_match.group(0)
|
|
|
|
# Pruefe dass overflow-y: auto vorhanden ist
|
|
assert 'overflow-y: auto' in sidebar_css or 'overflow-y:auto' in sidebar_css, \
|
|
"Sidebar muss overflow-y: auto haben fuer Scrollbarkeit"
|
|
|
|
def test_sidebar_has_overflow_x_hidden(self, studio_css):
|
|
"""
|
|
Testet, dass die Sidebar horizontal nicht scrollbar ist.
|
|
"""
|
|
sidebar_css_pattern = r'\.sidebar\s*\{[^}]*\}'
|
|
sidebar_css_match = re.search(sidebar_css_pattern, studio_css, re.DOTALL)
|
|
|
|
assert sidebar_css_match is not None, "Sidebar CSS-Block nicht gefunden"
|
|
|
|
sidebar_css_block = sidebar_css_match.group(0)
|
|
|
|
# Pruefe dass overflow-x: hidden vorhanden ist
|
|
assert 'overflow-x: hidden' in sidebar_css_block or 'overflow-x:hidden' in sidebar_css_block, \
|
|
"Sidebar muss overflow-x: hidden haben"
|
|
|
|
def test_sidebar_does_not_have_overflow_hidden_only(self, studio_css):
|
|
"""
|
|
Testet, dass die Sidebar NICHT nur overflow: hidden hat.
|
|
|
|
Regression Test: overflow: hidden wuerde verhindern,
|
|
dass Benutzer zu den GPU-Buttons scrollen koennen.
|
|
"""
|
|
sidebar_css_pattern = r'\.sidebar\s*\{[^}]*\}'
|
|
sidebar_css_match = re.search(sidebar_css_pattern, studio_css, re.DOTALL)
|
|
|
|
assert sidebar_css_match is not None, "Sidebar CSS-Block nicht gefunden"
|
|
|
|
sidebar_css_block = sidebar_css_match.group(0)
|
|
|
|
# Pruefe dass nicht einfach "overflow: hidden" steht (ohne x/y Spezifikation)
|
|
# Erlaubt sind: overflow-x: hidden, overflow-y: hidden, aber nicht nur "overflow: hidden"
|
|
has_overflow_hidden_only = re.search(r'overflow\s*:\s*hidden', sidebar_css_block) and \
|
|
not re.search(r'overflow-[xy]\s*:', sidebar_css_block)
|
|
|
|
assert not has_overflow_hidden_only, \
|
|
"Sidebar darf nicht nur 'overflow: hidden' haben - GPU Buttons waeren nicht erreichbar"
|
|
|
|
|
|
@pytest.mark.skip(reason="vast.ai GPU UI not yet implemented in frontend template")
|
|
class TestStudioGPUControls:
|
|
"""Tests fuer die GPU-Kontrollelemente.
|
|
|
|
HINWEIS: Diese Tests sind fuer zukuenftige vast.ai GPU-UI-Elemente.
|
|
Die Backend-API existiert bereits (infra/vast_power.py), aber die
|
|
Frontend-UI-Elemente wurden noch nicht in studio.html implementiert.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def studio_html(self):
|
|
"""Laedt den HTML-Inhalt aus dem Template."""
|
|
return TEMPLATE_HTML.read_text(encoding="utf-8")
|
|
|
|
def test_gpu_start_button_exists(self, studio_html):
|
|
"""Testet, dass der GPU Start-Button existiert."""
|
|
assert 'id="btn-vast-start"' in studio_html, \
|
|
"GPU Start-Button (btn-vast-start) nicht gefunden"
|
|
|
|
def test_gpu_stop_button_exists(self, studio_html):
|
|
"""Testet, dass der GPU Stop-Button existiert."""
|
|
assert 'id="btn-vast-stop"' in studio_html, \
|
|
"GPU Stop-Button (btn-vast-stop) nicht gefunden"
|
|
|
|
def test_gpu_status_badge_exists(self, studio_html):
|
|
"""Testet, dass der GPU Status-Badge existiert."""
|
|
assert 'id="vast-status-badge"' in studio_html, \
|
|
"GPU Status-Badge nicht gefunden"
|
|
|
|
def test_gpu_buttons_are_in_sidebar(self, studio_html):
|
|
"""
|
|
Testet, dass die GPU-Buttons innerhalb der Sidebar sind.
|
|
"""
|
|
# Finde die Sidebar-Sektion
|
|
sidebar_pattern = r'<aside[^>]*class="[^"]*sidebar[^"]*"[^>]*>.*?</aside>'
|
|
sidebar_match = re.search(sidebar_pattern, studio_html, re.DOTALL)
|
|
|
|
assert sidebar_match is not None, "Sidebar nicht gefunden"
|
|
|
|
sidebar_content = sidebar_match.group(0)
|
|
|
|
# Pruefe dass GPU-Buttons in der Sidebar sind
|
|
assert 'btn-vast-start' in sidebar_content, \
|
|
"GPU Start-Button muss in der Sidebar sein"
|
|
assert 'btn-vast-stop' in sidebar_content, \
|
|
"GPU Stop-Button muss in der Sidebar sein"
|
|
|
|
def test_gpu_cost_elements_exist(self, studio_html):
|
|
"""Testet, dass die Kostenanzeige-Elemente existieren."""
|
|
required_elements = [
|
|
'vast-cost-hour', # Kosten pro Stunde
|
|
'vast-credit', # Budget/Credit
|
|
'vast-session-cost', # Session-Kosten
|
|
]
|
|
|
|
for element_id in required_elements:
|
|
assert f'id="{element_id}"' in studio_html, \
|
|
f"Kostenelement {element_id} nicht gefunden"
|
|
|
|
|
|
@pytest.mark.skip(reason="vast.ai GPU UI not yet implemented in frontend template")
|
|
class TestStudioVastButtons:
|
|
"""Tests fuer die vast.ai Button-Styles.
|
|
|
|
HINWEIS: vast.ai UI-Elemente wurden noch nicht implementiert.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def studio_html(self):
|
|
"""Laedt den HTML-Inhalt aus dem Template."""
|
|
return TEMPLATE_HTML.read_text(encoding="utf-8")
|
|
|
|
@pytest.fixture
|
|
def studio_css(self):
|
|
"""Laedt den CSS-Inhalt aus der separaten CSS-Datei."""
|
|
return STATIC_CSS.read_text(encoding="utf-8")
|
|
|
|
def test_vast_buttons_container_exists(self, studio_html):
|
|
"""Testet, dass der vast-buttons Container existiert."""
|
|
assert 'class="vast-buttons"' in studio_html, \
|
|
"vast-buttons Container nicht gefunden"
|
|
|
|
def test_vast_buttons_css_exists(self, studio_css):
|
|
"""Testet, dass CSS fuer .vast-buttons existiert."""
|
|
assert '.vast-buttons' in studio_css, \
|
|
"CSS fuer .vast-buttons nicht gefunden"
|
|
|
|
|
|
class TestStudioStaticFilesIntegration:
|
|
"""Integration Tests fuer Static Files Serving."""
|
|
|
|
def test_main_py_mounts_static_files(self):
|
|
"""Testet, dass main.py die Static Files korrekt mountet."""
|
|
main_py = Path(__file__).parent.parent / "main.py"
|
|
main_content = main_py.read_text(encoding="utf-8")
|
|
|
|
assert 'StaticFiles' in main_content, \
|
|
"main.py muss StaticFiles importieren"
|
|
assert 'app.mount("/static"' in main_content or "app.mount('/static'" in main_content, \
|
|
"main.py muss /static mounten"
|
|
|
|
def test_static_directory_structure(self):
|
|
"""Testet, dass die Static-Verzeichnisstruktur korrekt ist."""
|
|
static_dir = FRONTEND_DIR / "static"
|
|
assert static_dir.exists(), "static/ Verzeichnis muss existieren"
|
|
assert (static_dir / "css").exists(), "static/css/ Verzeichnis muss existieren"
|
|
assert (static_dir / "js").exists(), "static/js/ Verzeichnis muss existieren"
|
|
|
|
|
|
if __name__ == '__main__':
|
|
pytest.main([__file__, '-v'])
|