diff --git a/backend/README_FORM_TESTER.md b/backend/README_FORM_TESTER.md
new file mode 100644
index 000000000..82244ffed
--- /dev/null
+++ b/backend/README_FORM_TESTER.md
@@ -0,0 +1,393 @@
+# Flask HTML-Formular Test Automator 🧪
+
+Ein umfassender Frontend-fokussierter Test-Automator für HTML-Formulare in Flask-Anwendungen. Simuliert echte Browser-Interaktionen und testet die tatsächliche Benutzeroberfläche mit JavaScript-Validierungen, dynamischen Elementen und realistischen User-Interaktionen.
+
+## 🎯 Hauptmerkmale
+
+### ✅ **Browser-basiertes Testing**
+- Echte Browser-Interaktionen (Playwright)
+- Realistische Mausbewegungen und Tipp-Geschwindigkeiten
+- JavaScript-Validierungen werden mitgetestet
+- Dynamische Inhalte und AJAX-Requests
+
+### ✅ **Intelligente Formular-Erkennung**
+- Automatisches Scannen aller Formulare auf einer Seite
+- Erkennung von Feld-Typen und Validierungsregeln
+- Multi-Step-Formular-Unterstützung
+- CSRF-Token-Behandlung
+
+### ✅ **Realistische Test-Daten**
+- Intelligente Daten-Generierung basierend auf Feld-Attributen
+- Pattern-basierte Generierung (RegEx-Support)
+- Edge-Case-Generierung für robuste Tests
+- Locale-spezifische Daten (Deutsch, Englisch, etc.)
+
+### ✅ **Umfassende Validierung**
+- HTML5-Validierungen
+- Client-Side JavaScript-Validierungen
+- Visual Feedback (Fehlermeldungen, Icons)
+- Server-Side Validierung-Tests
+
+### ✅ **Accessibility-Testing**
+- ARIA-Attribute-Validierung
+- Label-Zuordnungs-Tests
+- Keyboard-Navigation
+- Farbkontrast-Prüfung
+- Screen-Reader-Kompatibilität
+
+### ✅ **Responsive Design Tests**
+- Multi-Device-Testing (iPhone, iPad, Desktop)
+- Touch-Interaktions-Tests
+- Viewport-spezifische Validierung
+- Layout-Stabilitäts-Tests
+
+### ✅ **Visual Documentation**
+- Screenshots aller Test-Phasen
+- Video-Aufzeichnung (optional)
+- HTML-Reports mit visuellen Beweisen
+- JSON-Export für Automation
+
+## 🚀 Installation
+
+### Schnell-Setup
+```bash
+# Setup-Skript ausführen
+./form_tester_setup.sh
+
+# Oder mit Virtual Environment
+./form_tester_setup.sh --venv
+```
+
+### Manuelle Installation
+```bash
+# Dependencies installieren
+pip install playwright faker beautifulsoup4 rich
+
+# Browser installieren
+playwright install chromium firefox webkit
+```
+
+## 📋 Verwendung
+
+### CLI-Interface
+
+#### Alle Formulare auf einer Seite testen
+```bash
+python form_test_automator.py --url http://localhost:5000/register --test-all
+```
+
+#### Spezifisches Formular testen
+```bash
+python form_test_automator.py --url http://localhost:5000/contact --form "#contact-form"
+```
+
+#### Multi-Step Formular testen
+```bash
+python form_test_automator.py --url http://localhost:5000/signup --multi-step --steps "step1,step2,step3"
+```
+
+#### Responsive Testing
+```bash
+python form_test_automator.py --url http://localhost:5000/order --responsive --devices "iPhone 12,iPad,Desktop"
+```
+
+#### Umfassende Tests mit allen Szenarien
+```bash
+python form_test_automator.py --url http://localhost:5000/application --comprehensive
+```
+
+#### Mit sichtbarem Browser (für Debugging)
+```bash
+python form_test_automator.py --url http://localhost:5000/login --test-all --headed
+```
+
+### Python-API
+
+```python
+import asyncio
+from form_test_automator import HTMLFormTestAutomator
+
+async def test_my_forms():
+ async with HTMLFormTestAutomator(
+ base_url="http://localhost:5000",
+ browser="chromium",
+ headless=True
+ ) as automator:
+
+ # Einfacher Formular-Test
+ results = await automator.test_form(
+ url="/login",
+ form_selector="#login-form",
+ test_data={
+ "username": "admin",
+ "password": "admin123"
+ },
+ test_scenarios=["valid", "invalid", "accessibility"]
+ )
+
+ # Report generieren
+ report_path = automator.generate_report("my_test_report")
+ print(f"Report: {report_path}")
+
+# Test ausführen
+asyncio.run(test_my_forms())
+```
+
+## 🧪 Test-Szenarien
+
+### Standard-Szenarien
+- **`valid`**: Gültige Daten, erwartete erfolgreiche Submission
+- **`invalid`**: Ungültige Daten, Validierung sollte greifen
+- **`edge_cases`**: XSS, SQL-Injection, Unicode, extreme Längen
+- **`accessibility`**: ARIA, Labels, Keyboard-Navigation
+- **`validations`**: Client-Side Validierungs-Tests
+- **`responsive`**: Multi-Device-Tests
+- **`dynamic`**: Conditional Fields, JavaScript-Verhalten
+- **`error_display`**: Fehlermeldungs-Anzeige-Tests
+
+### Erweiterte Tests
+```bash
+# Alle Szenarien
+python form_test_automator.py --url http://localhost:5000 --comprehensive
+
+# Spezifische Szenarien
+python form_test_automator.py --url http://localhost:5000 --scenarios "valid,accessibility,responsive"
+```
+
+## 📊 Reports
+
+### HTML-Report
+- Visuelle Übersicht aller Tests
+- Screenshots der Formular-Zustände
+- Interaktive Statistiken
+- Filtermöglichkeiten nach Status
+
+### JSON-Report
+- Maschinell lesbare Ergebnisse
+- Integration in CI/CD-Pipelines
+- Datenanalyse und Metriken
+- API-kompatibles Format
+
+### Screenshot-Dokumentation
+- Vor/Nach-Vergleiche
+- Fehler-Zustände
+- Responsive-Ansichten
+- Validierungs-Meldungen
+
+## 🎪 MYP-spezifische Tests
+
+Für das MYP (Manage Your Printers) System sind spezielle Test-Beispiele verfügbar:
+
+```bash
+# MYP-Beispiele ausführen
+python test_forms_example.py
+
+# Oder interaktiv wählen:
+# 1. Umfassende Tests (alle Formulare)
+# 2. Demo Einzeltest (mit sichtbarem Browser)
+# 3. Nur Login-Test
+# 4. Nur Responsive-Test
+```
+
+### Getestete MYP-Formulare
+- **Login & Authentifizierung**
+- **Drucker-Registrierung & -Verwaltung**
+- **Druckauftrags-Erstellung**
+- **Gast-Zugangs-System (OTP)**
+- **Einstellungen & Konfiguration**
+- **Tapo-Controller-Integration**
+
+## ⚙️ Konfiguration
+
+### form_tester_config.json
+```json
+{
+ "base_url": "http://localhost:5000",
+ "browser": "chromium",
+ "headless": true,
+ "viewport": {
+ "width": 1280,
+ "height": 720
+ },
+ "default_scenarios": [
+ "valid",
+ "invalid",
+ "accessibility"
+ ],
+ "test_devices": [
+ "iPhone SE",
+ "iPhone 12",
+ "iPad",
+ "Desktop 1280x720"
+ ],
+ "output_directory": "form_test_reports",
+ "screenshot_on_failure": true,
+ "video_recording": false
+}
+```
+
+## 🔧 Erweiterte Features
+
+### 1. Multi-Step-Form-Testing
+```python
+# Mehrstufige Formulare
+results = await automator.test_multi_step_form(
+ start_url="/registration",
+ steps=["personal-info", "address", "payment", "confirmation"]
+)
+```
+
+### 2. File-Upload-Testing
+```python
+# Datei-Upload-Tests
+test_data = {
+ "file": "/path/to/test-file.pdf",
+ "description": "Test-Upload"
+}
+```
+
+### 3. Cross-Browser-Testing
+```python
+browsers = ["chromium", "firefox", "webkit"]
+for browser in browsers:
+ async with HTMLFormTestAutomator(browser=browser) as automator:
+ # Tests für jeden Browser
+```
+
+### 4. Performance-Monitoring
+```python
+# Execution-Time wird automatisch gemessen
+for result in results:
+ print(f"Test: {result.test_type}, Zeit: {result.execution_time:.2f}s")
+```
+
+### 5. Custom Test Data Generators
+```python
+# Eigene Daten-Generatoren
+class CustomDataGenerator(FrontendTestDataGenerator):
+ def _generate_company_id(self, field):
+ return f"MB-{random.randint(1000, 9999)}"
+```
+
+## 🐛 Debugging & Troubleshooting
+
+### Häufige Probleme
+
+#### Playwright Installation
+```bash
+# Browser neu installieren
+playwright install chromium
+
+# System-Dependencies (Linux)
+sudo apt-get install libnss3 libatk-bridge2.0-0 libx11-xcb1
+```
+
+#### Formular nicht gefunden
+```python
+# Erweiterte Selektoren verwenden
+form_selector = "form[action*='login'], #login-form, .login-form"
+```
+
+#### JavaScript-Timing-Probleme
+```python
+# Längere Wartezeiten
+await page.wait_for_timeout(2000)
+await page.wait_for_load_state("networkidle")
+```
+
+### Debug-Modus
+```bash
+# Mit sichtbarem Browser
+python form_test_automator.py --url http://localhost:5000 --headed
+
+# Mit Console-Output
+python form_test_automator.py --url http://localhost:5000 --verbose
+```
+
+## 📈 Integration in CI/CD
+
+### GitHub Actions
+```yaml
+name: Form Tests
+on: [push, pull_request]
+jobs:
+ test-forms:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.9'
+ - name: Install dependencies
+ run: |
+ pip install -r requirements_form_tester.txt
+ playwright install chromium
+ - name: Run form tests
+ run: |
+ python form_test_automator.py --url http://localhost:5000 --comprehensive
+ - name: Upload reports
+ uses: actions/upload-artifact@v3
+ with:
+ name: form-test-reports
+ path: form_test_reports/
+```
+
+### Jenkins Pipeline
+```groovy
+pipeline {
+ agent any
+ stages {
+ stage('Form Tests') {
+ steps {
+ sh 'python form_test_automator.py --url $TEST_URL --comprehensive'
+ publishHTML([
+ allowMissing: false,
+ alwaysLinkToLastBuild: true,
+ keepAll: true,
+ reportDir: 'form_test_reports/reports',
+ reportFiles: '*.html',
+ reportName: 'Form Test Report'
+ ])
+ }
+ }
+ }
+}
+```
+
+## 🤝 Contributing
+
+### Neue Test-Szenarien hinzufügen
+```python
+# In FormInteractionEngine
+async def test_custom_scenario(self, form: FormStructure) -> List[TestResult]:
+ # Custom Test-Logik
+ pass
+```
+
+### Custom Validators
+```python
+# In VisualFormValidator
+async def validate_custom_aspect(self, form: FormStructure) -> List[TestResult]:
+ # Custom Validierung
+ pass
+```
+
+## 📝 Lizenz
+
+Dieses Tool wurde als Teil der Mercedes-Benz Projektarbeit MYP entwickelt.
+
+**Autor**: Till Tomczak
+**Projekt**: MYP (Manage Your Printers)
+**Version**: 1.0
+
+## 🔗 Links
+
+- [Playwright Documentation](https://playwright.dev/python/)
+- [Faker Documentation](https://faker.readthedocs.io/)
+- [BeautifulSoup Documentation](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)
+- [Rich Documentation](https://rich.readthedocs.io/)
+
+---
+
+**🎯 Frontend-fokussiertes Testing für Flask-Anwendungen - Teste wie deine Benutzer es sehen!**
\ No newline at end of file
diff --git a/backend/form_test_automator.py b/backend/form_test_automator.py
new file mode 100644
index 000000000..b7005aa26
--- /dev/null
+++ b/backend/form_test_automator.py
@@ -0,0 +1,2465 @@
+#!/usr/bin/env python3
+"""
+Flask HTML-Formular Test Automator - Frontend-Fokussierter Browser-Tester
+=========================================================================
+
+Automatisierte Tests für HTML-Formulare durch echte Browser-Interaktionen.
+Testet die tatsächliche Benutzeroberfläche mit JavaScript-Validierungen,
+dynamischen Elementen und realistischen User-Interaktionen.
+
+Autor: Till Tomczak
+Version: 1.0
+Mercedes-Benz Projektarbeit MYP
+"""
+
+import asyncio
+import json
+import time
+import random
+import re
+from pathlib import Path
+from datetime import datetime
+from typing import Dict, List, Optional, Any, Tuple
+from dataclasses import dataclass, asdict
+from enum import Enum
+
+# Browser-Automation
+try:
+ from playwright.async_api import async_playwright, Page, Browser, BrowserContext
+ PLAYWRIGHT_AVAILABLE = True
+except ImportError:
+ PLAYWRIGHT_AVAILABLE = False
+ print("⚠️ Playwright nicht verfügbar. Installiere mit: pip install playwright")
+
+# Test-Daten-Generierung
+try:
+ from faker import Faker
+ FAKER_AVAILABLE = True
+except ImportError:
+ FAKER_AVAILABLE = False
+ print("⚠️ Faker nicht verfügbar. Installiere mit: pip install faker")
+
+# HTML-Parsing
+try:
+ from bs4 import BeautifulSoup
+ BS4_AVAILABLE = True
+except ImportError:
+ BS4_AVAILABLE = False
+ print("⚠️ BeautifulSoup nicht verfügbar. Installiere mit: pip install beautifulsoup4")
+
+# Rich Console für schöne Ausgabe
+try:
+ from rich.console import Console
+ from rich.table import Table
+ from rich.progress import Progress
+ from rich.panel import Panel
+ from rich.text import Text
+ RICH_AVAILABLE = True
+ console = Console()
+except ImportError:
+ RICH_AVAILABLE = False
+ console = None
+
+
+class TestStatus(Enum):
+ """Status-Enumerierung für Tests"""
+ PASSED = "✅ BESTANDEN"
+ FAILED = "❌ FEHLGESCHLAGEN"
+ WARNING = "⚠️ WARNUNG"
+ SKIPPED = "⏭️ ÜBERSPRUNGEN"
+
+
+@dataclass
+class FormField:
+ """Repräsentiert ein Formular-Feld mit allen Attributen"""
+ name: str
+ field_type: str
+ selector: str
+ label: str = ""
+ placeholder: str = ""
+ required: bool = False
+ pattern: str = ""
+ min_length: int = 0
+ max_length: int = 0
+ min_value: str = ""
+ max_value: str = ""
+ options: List[str] = None
+ validation_message: str = ""
+
+ def __post_init__(self):
+ if self.options is None:
+ self.options = []
+
+
+@dataclass
+class FormStructure:
+ """Repräsentiert die Struktur eines HTML-Formulars"""
+ selector: str
+ action: str
+ method: str
+ fields: List[FormField]
+ submit_button: str = ""
+ is_multi_step: bool = False
+ csrf_token_field: str = ""
+ enctype: str = "application/x-www-form-urlencoded"
+
+
+@dataclass
+class TestResult:
+ """Ergebnis eines Formular-Tests"""
+ form_selector: str
+ test_type: str
+ status: TestStatus
+ message: str
+ screenshot_path: str = ""
+ execution_time: float = 0.0
+ details: Dict[str, Any] = None
+
+ def __post_init__(self):
+ if self.details is None:
+ self.details = {}
+
+
+class FrontendTestDataGenerator:
+ """
+ Generiert realistische Test-Daten für HTML-Formulare basierend auf
+ Feld-Attributen und semantischen Hinweisen.
+ """
+
+ def __init__(self, locale: str = 'de_DE'):
+ self.fake = Faker(locale) if FAKER_AVAILABLE else None
+
+ # Mapping von Input-Types zu Generatoren
+ self.type_generators = {
+ 'email': self._generate_email,
+ 'password': self._generate_password,
+ 'tel': self._generate_phone,
+ 'url': self._generate_url,
+ 'date': self._generate_date,
+ 'datetime-local': self._generate_datetime,
+ 'time': self._generate_time,
+ 'number': self._generate_number,
+ 'range': self._generate_range,
+ 'color': self._generate_color,
+ 'text': self._generate_text,
+ 'textarea': self._generate_text
+ }
+
+ # Pattern-basierte Generatoren
+ self.pattern_generators = {
+ r'[0-9]{5}': lambda: str(random.randint(10000, 99999)), # PLZ
+ r'[0-9]{3,4}': lambda: str(random.randint(100, 9999)), # Kurze Zahlen
+ r'[A-Za-z]+': lambda: self.fake.word() if self.fake else "TestWort",
+ r'[A-Z]{2,3}': lambda: ''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ', k=3))
+ }
+
+ def generate_for_field(self, field: FormField, scenario: str = 'valid') -> str:
+ """
+ Generiert Test-Daten für ein spezifisches Feld.
+
+ Args:
+ field: Das Formular-Feld
+ scenario: 'valid', 'invalid', 'edge_case', 'empty'
+ """
+ if scenario == 'empty':
+ return ""
+
+ # Erst Pattern prüfen
+ if field.pattern and scenario == 'valid':
+ for pattern, generator in self.pattern_generators.items():
+ if re.match(pattern, field.pattern):
+ return generator()
+
+ # Dann Type-basierten Generator verwenden
+ generator = self.type_generators.get(field.field_type, self._generate_text)
+
+ if scenario == 'valid':
+ return generator(field)
+ elif scenario == 'invalid':
+ return self._generate_invalid_data(field)
+ elif scenario == 'edge_case':
+ return self._generate_edge_case(field)
+
+ return generator(field)
+
+ def _generate_email(self, field: FormField = None) -> str:
+ if self.fake:
+ return self.fake.email()
+ return f"test{random.randint(1000, 9999)}@example.com"
+
+ def _generate_password(self, field: FormField = None) -> str:
+ """Generiert sichere Passwörter"""
+ if field and field.min_length > 0:
+ length = max(field.min_length, 8)
+ else:
+ length = 12
+
+ chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
+ return ''.join(random.choices(chars, k=length))
+
+ def _generate_phone(self, field: FormField = None) -> str:
+ if self.fake:
+ return self.fake.phone_number()
+ return f"+49 {random.randint(100, 999)} {random.randint(1000000, 9999999)}"
+
+ def _generate_url(self, field: FormField = None) -> str:
+ if self.fake:
+ return self.fake.url()
+ return f"https://example{random.randint(1, 100)}.com"
+
+ def _generate_date(self, field: FormField = None) -> str:
+ if self.fake:
+ return self.fake.date()
+ return "2024-06-18"
+
+ def _generate_datetime(self, field: FormField = None) -> str:
+ return f"{self._generate_date()}T{self._generate_time()}"
+
+ def _generate_time(self, field: FormField = None) -> str:
+ return f"{random.randint(0, 23):02d}:{random.randint(0, 59):02d}"
+
+ def _generate_number(self, field: FormField) -> str:
+ if field.min_value and field.max_value:
+ try:
+ min_val = int(field.min_value)
+ max_val = int(field.max_value)
+ return str(random.randint(min_val, max_val))
+ except ValueError:
+ pass
+ return str(random.randint(1, 1000))
+
+ def _generate_range(self, field: FormField) -> str:
+ return self._generate_number(field)
+
+ def _generate_color(self, field: FormField = None) -> str:
+ return f"#{random.randint(0, 0xFFFFFF):06x}"
+
+ def _generate_text(self, field: FormField) -> str:
+ """Generiert Text basierend auf Feld-Eigenschaften"""
+
+ # Intelligente Generierung basierend auf Namen/Labels
+ field_name_lower = field.name.lower()
+ label_lower = field.label.lower() if field.label else ""
+
+ if any(keyword in field_name_lower + label_lower for keyword in ['name', 'vorname', 'nachname', 'firma']):
+ if self.fake:
+ if 'vorname' in field_name_lower or 'first' in field_name_lower:
+ return self.fake.first_name()
+ elif 'nachname' in field_name_lower or 'last' in field_name_lower:
+ return self.fake.last_name()
+ elif 'firma' in field_name_lower or 'company' in field_name_lower:
+ return self.fake.company()
+ else:
+ return self.fake.name()
+
+ if any(keyword in field_name_lower + label_lower for keyword in ['stadt', 'city', 'ort']):
+ return self.fake.city() if self.fake else "München"
+
+ if any(keyword in field_name_lower + label_lower for keyword in ['straße', 'adresse', 'address']):
+ return self.fake.street_address() if self.fake else "Teststraße 123"
+
+ # Standard-Text basierend auf Länge
+ if field.max_length > 0:
+ if field.max_length <= 20:
+ return "Kurzer Test"
+ elif field.max_length <= 100:
+ return "Dies ist ein mittellanger Testtext für das Formular."
+ else:
+ return self.fake.text(max_nb_chars=field.max_length) if self.fake else "Langer Testtext " * 10
+
+ return "Test Eingabe"
+
+ def _generate_invalid_data(self, field: FormField) -> str:
+ """Generiert bewusst ungültige Daten für Validierungstests"""
+
+ if field.field_type == 'email':
+ return "ungueltige-email"
+ elif field.field_type == 'url':
+ return "keine-url"
+ elif field.field_type == 'number':
+ return "nicht-numerisch"
+ elif field.pattern:
+ return "passt-nicht-zum-pattern"
+ elif field.max_length > 0:
+ return "x" * (field.max_length + 10) # Zu lang
+
+ return "🚫 Ungültige Eingabe ñäöü"
+
+ def _generate_edge_case(self, field: FormField) -> str:
+ """Generiert Edge-Cases für robuste Tests"""
+
+ edge_cases = [
+ "' OR 1=1 --", # SQL Injection
+ "", # XSS
+ "../../etc/passwd", # Path Traversal
+ "NULL\x00", # Null Byte
+ "ñäöüßÀÁÂÃÄÅ", # Unicode
+ "👨💻🚀💻", # Emojis
+ "\n\r\t", # Whitespace
+ " ", # Nur Leerzeichen
+ ]
+
+ return random.choice(edge_cases)
+
+
+class FormInteractionEngine:
+ """
+ Simuliert realistische Benutzer-Interaktionen mit HTML-Formularen.
+ Verwendet menschliche Timing-Muster und natürliche Mausbewegungen.
+ """
+
+ def __init__(self, page: Page):
+ self.page = page
+ self.human_delay_min = 50 # Minimale Tipp-Verzögerung (ms)
+ self.human_delay_max = 150 # Maximale Tipp-Verzögerung (ms)
+
+ async def fill_form_like_human(self, form: FormStructure, test_data: Dict[str, str]) -> List[TestResult]:
+ """
+ Füllt ein Formular wie ein echter Mensch aus.
+
+ Args:
+ form: Die Formular-Struktur
+ test_data: Die einzutragenden Daten
+ """
+ results = []
+
+ try:
+ # Formular in Sicht scrollen
+ await self.page.locator(form.selector).scroll_into_view_if_needed()
+ await self._human_pause()
+
+ for field in form.fields:
+ if field.name in test_data:
+ result = await self._fill_field_humanlike(field, test_data[field.name])
+ results.append(result)
+
+ return results
+
+ except Exception as e:
+ return [TestResult(
+ form_selector=form.selector,
+ test_type="form_filling",
+ status=TestStatus.FAILED,
+ message=f"Fehler beim Ausfüllen: {str(e)}"
+ )]
+
+ async def _fill_field_humanlike(self, field: FormField, value: str) -> TestResult:
+ """Füllt ein einzelnes Feld mit menschlichem Verhalten"""
+
+ try:
+ field_element = self.page.locator(field.selector)
+
+ # Warten bis Feld sichtbar ist
+ await field_element.wait_for(state="visible", timeout=5000)
+
+ # Zu Feld scrollen
+ await field_element.scroll_into_view_if_needed()
+ await self._human_pause(100, 300)
+
+ # Feld anklicken (nicht nur fokussieren)
+ await field_element.click()
+ await self._human_pause()
+
+ # Existierenden Inhalt löschen
+ await self.page.keyboard.press("Control+a")
+ await self._human_pause(50, 100)
+
+ # Spezielle Behandlung nach Feld-Typ
+ if field.field_type in ['select', 'select-one']:
+ await self._select_option_humanlike(field_element, value)
+ elif field.field_type == 'checkbox':
+ if value.lower() in ['true', '1', 'on', 'yes']:
+ await field_element.check()
+ elif field.field_type == 'radio':
+ await field_element.check()
+ elif field.field_type == 'file':
+ await field_element.set_input_files(value)
+ else:
+ # Text-Input mit menschlicher Geschwindigkeit
+ await self._type_like_human(value)
+
+ # Kurze Pause nach Eingabe
+ await self._human_pause()
+
+ # Tab zur nächsten Feld (menschliches Verhalten)
+ await self.page.keyboard.press("Tab")
+
+ return TestResult(
+ form_selector=field.selector,
+ test_type="field_filling",
+ status=TestStatus.PASSED,
+ message=f"Feld '{field.name}' erfolgreich ausgefüllt"
+ )
+
+ except Exception as e:
+ return TestResult(
+ form_selector=field.selector,
+ test_type="field_filling",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Feld '{field.name}': {str(e)}"
+ )
+
+ async def _select_option_humanlike(self, select_element, value: str):
+ """Wählt Option in Select-Element wie ein Mensch"""
+
+ # Dropdown öffnen
+ await select_element.click()
+ await self._human_pause(200, 400)
+
+ # Option suchen und klicken
+ option_selector = f"option[value='{value}'], option:has-text('{value}')"
+ try:
+ await self.page.click(option_selector)
+ except:
+ # Fallback: erste Option wählen
+ await self.page.click("option:first-child")
+
+ async def _type_like_human(self, text: str):
+ """Tippt Text mit menschlicher Geschwindigkeit und gelegentlichen Fehlern"""
+
+ for char in text:
+ # Gelegentliche "Tippfehler" und Korrekturen (5% Chance)
+ if random.random() < 0.05 and char.isalpha():
+ # Falscher Buchstabe
+ wrong_char = random.choice('abcdefghijklmnopqrstuvwxyz')
+ await self.page.keyboard.type(wrong_char, delay=self._random_delay())
+ await self._human_pause(100, 200)
+
+ # Korrigieren
+ await self.page.keyboard.press("Backspace")
+ await self._human_pause(50, 150)
+
+ # Richtiger Buchstabe
+ await self.page.keyboard.type(char, delay=self._random_delay())
+
+ # Gelegentliche längere Pausen (Nachdenken)
+ if random.random() < 0.1:
+ await self._human_pause(500, 1000)
+
+ async def test_form_validations(self, form: FormStructure) -> List[TestResult]:
+ """
+ Testet alle visuellen Validierungen eines Formulars.
+ """
+ results = []
+
+ # 1. Teste Required-Feld-Validierungen
+ required_test = await self._test_required_fields(form)
+ results.extend(required_test)
+
+ # 2. Teste Pattern-Validierungen
+ pattern_test = await self._test_pattern_validations(form)
+ results.extend(pattern_test)
+
+ # 3. Teste Längen-Validierungen
+ length_test = await self._test_length_validations(form)
+ results.extend(length_test)
+
+ return results
+
+ async def _test_required_fields(self, form: FormStructure) -> List[TestResult]:
+ """Testet Required-Feld-Validierungen"""
+ results = []
+
+ try:
+ # Versuche zu submitten ohne Required-Felder
+ if form.submit_button:
+ await self.page.click(form.submit_button)
+ await self._human_pause(500, 1000)
+
+ # Prüfe auf Validierungsmeldungen
+ for field in form.fields:
+ if field.required:
+ validation_visible = await self._check_validation_message_visible(field)
+
+ status = TestStatus.PASSED if validation_visible else TestStatus.FAILED
+ message = "Required-Validierung sichtbar" if validation_visible else "Keine Required-Validierung gefunden"
+
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="required_validation",
+ status=status,
+ message=message
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type="required_validation",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Required-Test: {str(e)}"
+ ))
+
+ return results
+
+ async def _test_pattern_validations(self, form: FormStructure) -> List[TestResult]:
+ """Testet Pattern-basierte Validierungen"""
+ results = []
+
+ for field in form.fields:
+ if field.pattern:
+ try:
+ # Ungültiges Pattern eingeben
+ await self.page.fill(field.selector, "ungültiges-pattern")
+ await self.page.keyboard.press("Tab") # Trigger Validation
+ await self._human_pause(300, 500)
+
+ # Prüfe Validierungsmeldung
+ validation_visible = await self._check_validation_message_visible(field)
+
+ status = TestStatus.PASSED if validation_visible else TestStatus.FAILED
+ message = f"Pattern-Validierung für '{field.pattern}' funktioniert" if validation_visible else f"Pattern-Validierung für '{field.pattern}' fehlt"
+
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="pattern_validation",
+ status=status,
+ message=message
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="pattern_validation",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Pattern-Test: {str(e)}"
+ ))
+
+ return results
+
+ async def _test_length_validations(self, form: FormStructure) -> List[TestResult]:
+ """Testet Längen-Validierungen"""
+ results = []
+
+ for field in form.fields:
+ if field.max_length > 0:
+ try:
+ # Zu langen Text eingeben
+ long_text = "x" * (field.max_length + 10)
+ await self.page.fill(field.selector, long_text)
+ await self.page.keyboard.press("Tab")
+ await self._human_pause(300, 500)
+
+ # Prüfe ob Text abgeschnitten wurde oder Validierung angezeigt wird
+ actual_value = await self.page.input_value(field.selector)
+ validation_visible = await self._check_validation_message_visible(field)
+
+ if len(actual_value) <= field.max_length or validation_visible:
+ status = TestStatus.PASSED
+ message = f"Längen-Validierung (max: {field.max_length}) funktioniert"
+ else:
+ status = TestStatus.FAILED
+ message = f"Längen-Validierung (max: {field.max_length}) nicht wirksam"
+
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="length_validation",
+ status=status,
+ message=message
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="length_validation",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Längen-Test: {str(e)}"
+ ))
+
+ return results
+
+ async def _check_validation_message_visible(self, field: FormField) -> bool:
+ """Prüft ob eine Validierungsmeldung sichtbar ist"""
+
+ # Verschiedene Selektoren für Validierungsmeldungen
+ validation_selectors = [
+ f"{field.selector}:invalid",
+ f"{field.selector} + .error",
+ f"{field.selector} + .invalid-feedback",
+ f"{field.selector} ~ .error-message",
+ ".field-error",
+ ".validation-error",
+ "[role='alert']",
+ ".alert-danger",
+ ".text-danger"
+ ]
+
+ for selector in validation_selectors:
+ try:
+ element = self.page.locator(selector)
+ if await element.count() > 0 and await element.is_visible():
+ return True
+ except:
+ continue
+
+ # Prüfe auch HTML5-Validierungsmeldungen
+ try:
+ validation_message = await self.page.evaluate(f"""
+ document.querySelector('{field.selector}').validationMessage
+ """)
+ return bool(validation_message)
+ except:
+ pass
+
+ return False
+
+ async def test_dynamic_form_behavior(self, form: FormStructure) -> List[TestResult]:
+ """
+ Testet dynamisches Formular-Verhalten wie Conditional Fields.
+ """
+ results = []
+
+ # Teste JavaScript-basierte Feld-Anzeige/Verstecken
+ initial_field_count = len([f for f in form.fields if await self.page.locator(f.selector).is_visible()])
+
+ # Interagiere mit Feldern und prüfe Änderungen
+ for field in form.fields:
+ if field.field_type in ['select', 'radio', 'checkbox']:
+ try:
+ # Zustand vor Änderung
+ visible_fields_before = await self._get_visible_field_count(form)
+
+ # Feld ändern
+ if field.field_type == 'checkbox':
+ await self.page.locator(field.selector).check()
+ elif field.options:
+ await self.page.select_option(field.selector, field.options[0])
+
+ await self._human_pause(500, 1000) # Warten auf JavaScript
+
+ # Zustand nach Änderung
+ visible_fields_after = await self._get_visible_field_count(form)
+
+ if visible_fields_before != visible_fields_after:
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="dynamic_behavior",
+ status=TestStatus.PASSED,
+ message=f"Dynamisches Verhalten erkannt: {visible_fields_before} → {visible_fields_after} Felder"
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="dynamic_behavior",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Dynamic-Test: {str(e)}"
+ ))
+
+ return results
+
+ async def _get_visible_field_count(self, form: FormStructure) -> int:
+ """Zählt sichtbare Felder in einem Formular"""
+ count = 0
+ for field in form.fields:
+ try:
+ if await self.page.locator(field.selector).is_visible():
+ count += 1
+ except:
+ pass
+ return count
+
+ async def _human_pause(self, min_ms: int = 100, max_ms: int = 300):
+ """Simuliert menschliche Pausen"""
+ delay = random.randint(min_ms, max_ms)
+ await asyncio.sleep(delay / 1000)
+
+ def _random_delay(self) -> int:
+ """Zufällige Tipp-Verzögerung"""
+ return random.randint(self.human_delay_min, self.human_delay_max)
+
+
+class VisualFormValidator:
+ """
+ Validiert visuelle Aspekte von Formularen wie Accessibility,
+ Responsive-Design und Benutzerfreundlichkeit.
+ """
+
+ def __init__(self, page: Page):
+ self.page = page
+
+ async def check_form_accessibility(self, form: FormStructure) -> List[TestResult]:
+ """
+ Prüft Accessibility-Aspekte eines Formulars.
+ """
+ results = []
+
+ # 1. Label-Zuordnung prüfen
+ label_results = await self._check_label_associations(form)
+ results.extend(label_results)
+
+ # 2. ARIA-Attribute prüfen
+ aria_results = await self._check_aria_attributes(form)
+ results.extend(aria_results)
+
+ # 3. Keyboard-Navigation testen
+ keyboard_results = await self._test_keyboard_navigation(form)
+ results.extend(keyboard_results)
+
+ # 4. Kontrast prüfen
+ contrast_results = await self._check_color_contrast(form)
+ results.extend(contrast_results)
+
+ return results
+
+ async def _check_label_associations(self, form: FormStructure) -> List[TestResult]:
+ """Prüft ob alle Formular-Felder korrekte Labels haben"""
+ results = []
+
+ for field in form.fields:
+ try:
+ # Verschiedene Label-Zuordnungsmethoden prüfen
+ has_label = await self.page.evaluate(f"""
+ (() => {{
+ const field = document.querySelector('{field.selector}');
+ if (!field) return false;
+
+ // 1. Explizites Label mit for-Attribut
+ const labelByFor = document.querySelector(`label[for="${{field.id}}"]`);
+ if (labelByFor) return true;
+
+ // 2. Umschließendes Label
+ const parentLabel = field.closest('label');
+ if (parentLabel) return true;
+
+ // 3. ARIA-Label
+ if (field.getAttribute('aria-label')) return true;
+
+ // 4. ARIA-Labelledby
+ if (field.getAttribute('aria-labelledby')) return true;
+
+ return false;
+ }})()
+ """)
+
+ status = TestStatus.PASSED if has_label else TestStatus.FAILED
+ message = "Label korrekt zugeordnet" if has_label else "Kein Label gefunden"
+
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="accessibility_labels",
+ status=status,
+ message=message
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="accessibility_labels",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Label-Prüfung: {str(e)}"
+ ))
+
+ return results
+
+ async def _check_aria_attributes(self, form: FormStructure) -> List[TestResult]:
+ """Prüft ARIA-Attribute für bessere Accessibility"""
+ results = []
+
+ for field in form.fields:
+ try:
+ aria_info = await self.page.evaluate(f"""
+ (() => {{
+ const field = document.querySelector('{field.selector}');
+ if (!field) return {{}};
+
+ return {{
+ hasAriaLabel: !!field.getAttribute('aria-label'),
+ hasAriaLabelledBy: !!field.getAttribute('aria-labelledby'),
+ hasAriaDescribedBy: !!field.getAttribute('aria-describedby'),
+ hasAriaRequired: !!field.getAttribute('aria-required'),
+ hasAriaInvalid: !!field.getAttribute('aria-invalid'),
+ role: field.getAttribute('role')
+ }};
+ }})()
+ """)
+
+ # Bewerte ARIA-Vollständigkeit
+ aria_score = sum([
+ aria_info.get('hasAriaLabel', False),
+ aria_info.get('hasAriaLabelledBy', False),
+ aria_info.get('hasAriaDescribedBy', False),
+ aria_info.get('hasAriaRequired', False) if field.required else True,
+ True # Basis-Punkt
+ ])
+
+ if aria_score >= 3:
+ status = TestStatus.PASSED
+ message = "ARIA-Attribute ausreichend vorhanden"
+ elif aria_score >= 2:
+ status = TestStatus.WARNING
+ message = "ARIA-Attribute teilweise vorhanden"
+ else:
+ status = TestStatus.FAILED
+ message = "ARIA-Attribute unzureichend"
+
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="accessibility_aria",
+ status=status,
+ message=message,
+ details=aria_info
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="accessibility_aria",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei ARIA-Prüfung: {str(e)}"
+ ))
+
+ return results
+
+ async def _test_keyboard_navigation(self, form: FormStructure) -> List[TestResult]:
+ """Testet Keyboard-Navigation durch das Formular"""
+ results = []
+
+ try:
+ # Zum ersten Feld navigieren
+ if form.fields:
+ first_field = form.fields[0]
+ await self.page.locator(first_field.selector).focus()
+
+ # Durch alle Felder tabben
+ focusable_count = 0
+ for i, field in enumerate(form.fields):
+ try:
+ # Tab-Taste drücken
+ if i > 0:
+ await self.page.keyboard.press("Tab")
+ await asyncio.sleep(0.1)
+
+ # Prüfen ob Feld fokussiert ist
+ is_focused = await self.page.evaluate(f"""
+ document.activeElement === document.querySelector('{field.selector}')
+ """)
+
+ if is_focused:
+ focusable_count += 1
+
+ except Exception:
+ pass
+
+ # Bewertung
+ expected_focusable = len([f for f in form.fields if f.field_type not in ['hidden']])
+ focus_ratio = focusable_count / expected_focusable if expected_focusable > 0 else 0
+
+ if focus_ratio >= 0.8:
+ status = TestStatus.PASSED
+ message = f"Keyboard-Navigation funktioniert ({focusable_count}/{expected_focusable} Felder erreichbar)"
+ elif focus_ratio >= 0.5:
+ status = TestStatus.WARNING
+ message = f"Keyboard-Navigation teilweise funktionsfähig ({focusable_count}/{expected_focusable} Felder)"
+ else:
+ status = TestStatus.FAILED
+ message = f"Keyboard-Navigation problematisch ({focusable_count}/{expected_focusable} Felder)"
+
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type="accessibility_keyboard",
+ status=status,
+ message=message
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type="accessibility_keyboard",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Keyboard-Test: {str(e)}"
+ ))
+
+ return results
+
+ async def _check_color_contrast(self, form: FormStructure) -> List[TestResult]:
+ """Prüft Farbkontrast für bessere Lesbarkeit"""
+ results = []
+
+ for field in form.fields:
+ try:
+ # Hole CSS-Eigenschaften
+ styles = await self.page.evaluate(f"""
+ (() => {{
+ const field = document.querySelector('{field.selector}');
+ if (!field) return null;
+
+ const computed = getComputedStyle(field);
+ return {{
+ color: computed.color,
+ backgroundColor: computed.backgroundColor,
+ borderColor: computed.borderColor
+ }};
+ }})()
+ """)
+
+ if styles:
+ # Vereinfachte Kontrast-Bewertung
+ # (Eine echte Implementierung würde WCAG-Kontrast-Algorithmus verwenden)
+ has_good_contrast = True # Placeholder
+
+ status = TestStatus.PASSED if has_good_contrast else TestStatus.WARNING
+ message = "Farbkontrast ausreichend" if has_good_contrast else "Farbkontrast möglicherweise unzureichend"
+
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="accessibility_contrast",
+ status=status,
+ message=message,
+ details=styles
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=field.selector,
+ test_type="accessibility_contrast",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Kontrast-Prüfung: {str(e)}"
+ ))
+
+ return results
+
+ async def validate_error_display(self, form: FormStructure) -> List[TestResult]:
+ """
+ Prüft wie Fehlermeldungen angezeigt werden.
+ """
+ results = []
+
+ # Trigger Validierungsfehler
+ try:
+ # Submit ohne ausgefüllte Required-Felder
+ if form.submit_button:
+ await self.page.click(form.submit_button)
+ await asyncio.sleep(1) # Warten auf Validierung
+
+ # Prüfe Fehler-Display-Eigenschaften
+ error_display_quality = await self._analyze_error_display()
+
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type="error_display",
+ status=error_display_quality['status'],
+ message=error_display_quality['message'],
+ details=error_display_quality['details']
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type="error_display",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Error-Display-Test: {str(e)}"
+ ))
+
+ return results
+
+ async def _analyze_error_display(self) -> Dict[str, Any]:
+ """Analysiert die Qualität der Fehlerdarstellung"""
+
+ error_analysis = await self.page.evaluate("""
+ (() => {
+ const errorElements = document.querySelectorAll(
+ '.error, .invalid-feedback, .text-danger, [role="alert"], .field-error'
+ );
+
+ let visibleErrors = 0;
+ let totalErrors = errorElements.length;
+ let errorTexts = [];
+
+ errorElements.forEach(el => {
+ const isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;
+ if (isVisible) {
+ visibleErrors++;
+ errorTexts.push(el.textContent.trim());
+ }
+ });
+
+ return {
+ totalErrors,
+ visibleErrors,
+ errorTexts,
+ hasErrors: totalErrors > 0
+ };
+ })()
+ """)
+
+ if error_analysis['hasErrors']:
+ if error_analysis['visibleErrors'] > 0:
+ status = TestStatus.PASSED
+ message = f"Fehlermeldungen werden korrekt angezeigt ({error_analysis['visibleErrors']} sichtbar)"
+ else:
+ status = TestStatus.FAILED
+ message = "Fehlermeldungen vorhanden aber nicht sichtbar"
+ else:
+ status = TestStatus.WARNING
+ message = "Keine Fehlermeldungen gefunden (möglicherweise keine Validierung aktiv)"
+
+ return {
+ 'status': status,
+ 'message': message,
+ 'details': error_analysis
+ }
+
+ async def test_responsive_behavior(self, form: FormStructure, viewports: List[Dict] = None) -> List[TestResult]:
+ """
+ Testet Formular auf verschiedenen Bildschirmgrößen.
+ """
+ if not viewports:
+ viewports = [
+ {'width': 375, 'height': 667, 'name': 'iPhone SE'},
+ {'width': 768, 'height': 1024, 'name': 'iPad'},
+ {'width': 1920, 'height': 1080, 'name': 'Desktop'}
+ ]
+
+ results = []
+ original_viewport = self.page.viewport_size
+
+ for viewport in viewports:
+ try:
+ # Viewport setzen
+ await self.page.set_viewport_size(width=viewport['width'], height=viewport['height'])
+ await asyncio.sleep(0.5) # Layout stabilisieren
+
+ # Formular-Sichtbarkeit prüfen
+ form_visible = await self.page.locator(form.selector).is_visible()
+
+ # Feld-Zugänglichkeit prüfen
+ accessible_fields = 0
+ for field in form.fields:
+ try:
+ field_element = self.page.locator(field.selector)
+ if await field_element.is_visible():
+ # Prüfe ob Feld bedienbar ist
+ bounding_box = await field_element.bounding_box()
+ if bounding_box and bounding_box['width'] > 0 and bounding_box['height'] > 0:
+ accessible_fields += 1
+ except:
+ pass
+
+ # Bewertung
+ accessibility_ratio = accessible_fields / len(form.fields) if form.fields else 0
+
+ if form_visible and accessibility_ratio >= 0.9:
+ status = TestStatus.PASSED
+ message = f"Responsive Design funktioniert auf {viewport['name']}"
+ elif form_visible and accessibility_ratio >= 0.7:
+ status = TestStatus.WARNING
+ message = f"Responsive Design teilweise funktional auf {viewport['name']}"
+ else:
+ status = TestStatus.FAILED
+ message = f"Responsive Design problematisch auf {viewport['name']}"
+
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type=f"responsive_{viewport['name'].lower().replace(' ', '_')}",
+ status=status,
+ message=message,
+ details={
+ 'viewport': viewport,
+ 'form_visible': form_visible,
+ 'accessible_fields': accessible_fields,
+ 'total_fields': len(form.fields),
+ 'accessibility_ratio': accessibility_ratio
+ }
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type=f"responsive_{viewport['name'].lower().replace(' ', '_')}",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Responsive-Test auf {viewport['name']}: {str(e)}"
+ ))
+
+ # Ursprüngliches Viewport wiederherstellen
+ if original_viewport:
+ await self.page.set_viewport_size(
+ width=original_viewport['width'],
+ height=original_viewport['height']
+ )
+
+ return results
+
+
+class VisualTestReporter:
+ """
+ Erstellt umfassende Reports mit Screenshots, Videos und visuellen Beweisen.
+ """
+
+ def __init__(self, output_dir: Path = None):
+ self.output_dir = output_dir or Path("form_test_reports")
+ self.output_dir.mkdir(exist_ok=True)
+
+ self.screenshots_dir = self.output_dir / "screenshots"
+ self.videos_dir = self.output_dir / "videos"
+ self.reports_dir = self.output_dir / "reports"
+
+ for dir_path in [self.screenshots_dir, self.videos_dir, self.reports_dir]:
+ dir_path.mkdir(exist_ok=True)
+
+ async def capture_form_states(self, page: Page, form_selector: str, test_name: str) -> Dict[str, str]:
+ """
+ Dokumentiert verschiedene Formular-Zustände visuell.
+ """
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ base_name = f"{test_name}_{timestamp}"
+
+ screenshots = {}
+
+ try:
+ # 1. Initial State
+ initial_path = self.screenshots_dir / f"{base_name}_initial.png"
+ await page.screenshot(path=str(initial_path), full_page=True)
+ screenshots['initial'] = str(initial_path)
+
+ # 2. Form in Focus
+ form_element = page.locator(form_selector)
+ await form_element.scroll_into_view_if_needed()
+ await asyncio.sleep(0.5)
+
+ form_focus_path = self.screenshots_dir / f"{base_name}_form_focus.png"
+ await page.screenshot(path=str(form_focus_path), full_page=True)
+ screenshots['form_focus'] = str(form_focus_path)
+
+ # 3. Form Element Only (wenn möglich)
+ try:
+ form_only_path = self.screenshots_dir / f"{base_name}_form_only.png"
+ await form_element.screenshot(path=str(form_only_path))
+ screenshots['form_only'] = str(form_only_path)
+ except:
+ pass
+
+ return screenshots
+
+ except Exception as e:
+ print(f"Fehler beim Screenshot-Capturing: {e}")
+ return {}
+
+ async def capture_validation_errors(self, page: Page, test_name: str) -> str:
+ """
+ Erstellt Screenshots von Validierungsfehlern.
+ """
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ error_screenshot_path = self.screenshots_dir / f"{test_name}_validation_errors_{timestamp}.png"
+
+ try:
+ await page.screenshot(path=str(error_screenshot_path), full_page=True)
+ return str(error_screenshot_path)
+ except Exception as e:
+ print(f"Fehler beim Error-Screenshot: {e}")
+ return ""
+
+ async def start_video_recording(self, page: Page, test_name: str):
+ """Startet Video-Aufzeichnung (falls unterstützt)"""
+ try:
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ video_path = self.videos_dir / f"{test_name}_{timestamp}.webm"
+
+ # Playwright Video-Recording aktivieren (context-basiert)
+ # Dies muss beim Context-Setup konfiguriert werden
+ return str(video_path)
+ except Exception as e:
+ print(f"Video-Recording nicht verfügbar: {e}")
+ return None
+
+ def generate_visual_report(self, test_results: List[TestResult], report_name: str = "form_test_report") -> str:
+ """
+ Generiert einen HTML-Report mit visuellen Beweisen.
+ """
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ report_path = self.reports_dir / f"{report_name}_{timestamp}.html"
+
+ # Gruppiere Ergebnisse nach Form
+ forms_results = {}
+ for result in test_results:
+ form_id = result.form_selector
+ if form_id not in forms_results:
+ forms_results[form_id] = []
+ forms_results[form_id].append(result)
+
+ # HTML-Report generieren
+ html_content = self._generate_html_report(forms_results, report_name)
+
+ try:
+ with open(report_path, 'w', encoding='utf-8') as f:
+ f.write(html_content)
+
+ print(f"📊 HTML-Report erstellt: {report_path}")
+ return str(report_path)
+
+ except Exception as e:
+ print(f"Fehler beim Report-Generieren: {e}")
+ return ""
+
+ def _generate_html_report(self, forms_results: Dict[str, List[TestResult]], report_name: str) -> str:
+ """Generiert HTML-Inhalt für den Report"""
+
+ # Statistiken berechnen
+ total_tests = sum(len(results) for results in forms_results.values())
+ passed_tests = sum(1 for results in forms_results.values() for r in results if r.status == TestStatus.PASSED)
+ failed_tests = sum(1 for results in forms_results.values() for r in results if r.status == TestStatus.FAILED)
+ warning_tests = sum(1 for results in forms_results.values() for r in results if r.status == TestStatus.WARNING)
+
+ success_rate = (passed_tests / total_tests * 100) if total_tests > 0 else 0
+
+ html = f"""
+
+
+
+
+
+ {report_name} - Formular Test Report
+
+
+
+
+
+
+
+
+
{total_tests}
+
Gesamt Tests
+
+
+
{passed_tests}
+
Bestanden
+
+
+
{failed_tests}
+
Fehlgeschlagen
+
+
+
{warning_tests}
+
Warnungen
+
+
+
{success_rate:.1f}%
+
Erfolgsrate
+
+
+
+
+"""
+
+ # Formular-spezifische Ergebnisse
+ for form_selector, results in forms_results.items():
+ html += f"""
+
+"""
+
+ html += f"""
+
+
+
+
+
+
+"""
+
+ return html
+
+ def generate_json_report(self, test_results: List[TestResult], report_name: str = "form_test_report") -> str:
+ """Generiert einen JSON-Report für maschinelle Weiterverarbeitung"""
+
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ json_path = self.reports_dir / f"{report_name}_{timestamp}.json"
+
+ # Konvertiere Results zu Dict
+ results_data = []
+ for result in test_results:
+ result_dict = asdict(result)
+ result_dict['status'] = result.status.value # Enum zu String
+ results_data.append(result_dict)
+
+ report_data = {
+ 'report_name': report_name,
+ 'generated_at': datetime.now().isoformat(),
+ 'summary': {
+ 'total_tests': len(test_results),
+ 'passed': len([r for r in test_results if r.status == TestStatus.PASSED]),
+ 'failed': len([r for r in test_results if r.status == TestStatus.FAILED]),
+ 'warnings': len([r for r in test_results if r.status == TestStatus.WARNING]),
+ 'skipped': len([r for r in test_results if r.status == TestStatus.SKIPPED])
+ },
+ 'test_results': results_data
+ }
+
+ try:
+ with open(json_path, 'w', encoding='utf-8') as f:
+ json.dump(report_data, f, indent=2, ensure_ascii=False)
+
+ print(f"📋 JSON-Report erstellt: {json_path}")
+ return str(json_path)
+
+ except Exception as e:
+ print(f"Fehler beim JSON-Report: {e}")
+ return ""
+
+
+class HTMLFormTestAutomator:
+ """
+ Hauptklasse für automatisierte HTML-Formular-Tests.
+ Koordiniert alle Test-Komponenten und Browser-Interaktionen.
+ """
+
+ def __init__(self, base_url: str, browser: str = 'chromium', headless: bool = True):
+ self.base_url = base_url.rstrip('/')
+ self.browser_type = browser
+ self.headless = headless
+
+ # Komponenten
+ self.data_generator = FrontendTestDataGenerator()
+ self.reporter = VisualTestReporter()
+
+ # Browser-Instanzen (werden bei Bedarf initialisiert)
+ self.playwright = None
+ self.browser = None
+ self.context = None
+ self.page = None
+
+ # Test-Ergebnisse
+ self.test_results: List[TestResult] = []
+
+ # Validiere Voraussetzungen
+ if not PLAYWRIGHT_AVAILABLE:
+ raise ImportError("Playwright ist erforderlich. Installiere mit: pip install playwright")
+
+ async def __aenter__(self):
+ """Async Context Manager - Setup"""
+ await self._setup_browser()
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ """Async Context Manager - Cleanup"""
+ await self._cleanup_browser()
+
+ async def _setup_browser(self):
+ """Initialisiert Browser und Kontext"""
+ try:
+ self.playwright = await async_playwright().start()
+
+ # Browser starten
+ if self.browser_type == 'chromium':
+ self.browser = await self.playwright.chromium.launch(headless=self.headless)
+ elif self.browser_type == 'firefox':
+ self.browser = await self.playwright.firefox.launch(headless=self.headless)
+ elif self.browser_type == 'webkit':
+ self.browser = await self.playwright.webkit.launch(headless=self.headless)
+ else:
+ raise ValueError(f"Unbekannter Browser: {self.browser_type}")
+
+ # Kontext erstellen mit optimalen Einstellungen
+ self.context = await self.browser.new_context(
+ viewport={'width': 1280, 'height': 720},
+ ignore_https_errors=True, # Für lokale Entwicklung
+ record_video_dir=str(self.reporter.videos_dir) if not self.headless else None
+ )
+
+ # Seite erstellen
+ self.page = await self.context.new_page()
+
+ # Event-Handler für besseres Debugging
+ self.page.on("pageerror", lambda error: print(f"🚨 Browser-Fehler: {error}"))
+ self.page.on("console", lambda msg: print(f"🔍 Console: {msg.text}"))
+
+ print(f"✅ Browser {self.browser_type} gestartet (headless: {self.headless})")
+
+ except Exception as e:
+ await self._cleanup_browser()
+ raise Exception(f"Browser-Setup fehlgeschlagen: {str(e)}")
+
+ async def _cleanup_browser(self):
+ """Räumt Browser-Ressourcen auf"""
+ try:
+ if self.page:
+ await self.page.close()
+ if self.context:
+ await self.context.close()
+ if self.browser:
+ await self.browser.close()
+ if self.playwright:
+ await self.playwright.stop()
+ except Exception as e:
+ print(f"⚠️ Cleanup-Fehler: {e}")
+
+ async def scan_for_forms(self, page_url: str) -> List[FormStructure]:
+ """
+ Öffnet eine Seite und findet alle HTML-Formulare.
+
+ Args:
+ page_url: Relative oder absolute URL zur Seite
+ """
+ full_url = page_url if page_url.startswith('http') else f"{self.base_url}{page_url}"
+
+ try:
+ print(f"🔍 Scanne Formulare auf: {full_url}")
+
+ # Seite laden
+ response = await self.page.goto(full_url, wait_until="networkidle")
+ if response.status >= 400:
+ print(f"⚠️ HTTP {response.status}: {full_url}")
+
+ # Warten auf JavaScript und dynamische Inhalte
+ await asyncio.sleep(2)
+
+ # Formulare finden und analysieren
+ forms_data = await self.page.evaluate("""
+ () => {
+ const forms = Array.from(document.querySelectorAll('form'));
+
+ return forms.map((form, index) => {
+ const formId = form.id || form.className || `form-${index}`;
+ const selector = form.id ? `#${form.id}` :
+ form.className ? `.${form.className.split(' ')[0]}` :
+ `form:nth-of-type(${index + 1})`;
+
+ // Felder analysieren
+ const fields = Array.from(form.querySelectorAll(
+ 'input, textarea, select, [contenteditable="true"]'
+ )).map(field => {
+ const label = field.labels?.[0]?.textContent ||
+ field.getAttribute('aria-label') ||
+ field.previousElementSibling?.textContent ||
+ field.placeholder ||
+ field.name ||
+ field.id;
+
+ const options = field.tagName === 'SELECT' ?
+ Array.from(field.options).map(opt => opt.value) : [];
+
+ return {
+ name: field.name || field.id || `field-${Math.random().toString(36).substr(2, 9)}`,
+ field_type: field.type || field.tagName.toLowerCase(),
+ selector: field.id ? `#${field.id}` :
+ field.name ? `[name="${field.name}"]` :
+ field.className ? `.${field.className.split(' ')[0]}` :
+ `${field.tagName.toLowerCase()}:nth-of-type(${Array.from(form.querySelectorAll(field.tagName)).indexOf(field) + 1})`,
+ label: label?.trim() || '',
+ placeholder: field.placeholder || '',
+ required: field.required,
+ pattern: field.pattern || '',
+ min_length: parseInt(field.minLength) || 0,
+ max_length: parseInt(field.maxLength) || 0,
+ min_value: field.min || '',
+ max_value: field.max || '',
+ options: options
+ };
+ });
+
+ // Submit-Button finden
+ const submitButton = form.querySelector('[type="submit"], button[type="submit"], button:not([type])');
+ const submitSelector = submitButton ?
+ (submitButton.id ? `#${submitButton.id}` :
+ submitButton.className ? `.${submitButton.className.split(' ')[0]}` :
+ '[type="submit"]') : '';
+
+ // CSRF-Token finden
+ const csrfField = form.querySelector('[name*="csrf"], [name*="token"], [name="_token"]');
+ const csrfSelector = csrfField ? `[name="${csrfField.name}"]` : '';
+
+ return {
+ selector: selector,
+ action: form.action || window.location.href,
+ method: form.method || 'POST',
+ fields: fields,
+ submit_button: submitSelector,
+ is_multi_step: form.querySelectorAll('.step, .wizard-step, [data-step]').length > 1,
+ csrf_token_field: csrfSelector,
+ enctype: form.enctype || 'application/x-www-form-urlencoded'
+ };
+ });
+ }
+ """)
+
+ # Konvertiere zu FormStructure-Objekten
+ forms = []
+ for form_data in forms_data:
+ fields = [FormField(**field_data) for field_data in form_data['fields']]
+ form = FormStructure(
+ selector=form_data['selector'],
+ action=form_data['action'],
+ method=form_data['method'],
+ fields=fields,
+ submit_button=form_data['submit_button'],
+ is_multi_step=form_data['is_multi_step'],
+ csrf_token_field=form_data['csrf_token_field'],
+ enctype=form_data['enctype']
+ )
+ forms.append(form)
+
+ print(f"✅ {len(forms)} Formulare gefunden")
+ for i, form in enumerate(forms):
+ print(f" 📋 Form {i+1}: {form.selector} ({len(form.fields)} Felder)")
+
+ return forms
+
+ except Exception as e:
+ print(f"❌ Fehler beim Formular-Scan: {str(e)}")
+ return []
+
+ async def test_all_forms_on_page(self, page_url: str, test_scenarios: List[str] = None) -> List[TestResult]:
+ """
+ Testet alle Formulare auf einer Seite mit verschiedenen Szenarien.
+
+ Args:
+ page_url: URL der zu testenden Seite
+ test_scenarios: Liste der Test-Szenarien ['valid', 'invalid', 'edge_cases', 'accessibility']
+ """
+ if test_scenarios is None:
+ test_scenarios = ['valid', 'invalid', 'accessibility']
+
+ all_results = []
+
+ # Formulare finden
+ forms = await self.scan_for_forms(page_url)
+
+ if not forms:
+ return [TestResult(
+ form_selector=page_url,
+ test_type="form_discovery",
+ status=TestStatus.WARNING,
+ message="Keine Formulare auf der Seite gefunden"
+ )]
+
+ # Jedes Formular testen
+ for form in forms:
+ print(f"\n🧪 Teste Formular: {form.selector}")
+
+ form_results = await self.test_form_comprehensive(form, test_scenarios)
+ all_results.extend(form_results)
+
+ self.test_results.extend(all_results)
+ return all_results
+
+ async def test_form_comprehensive(self, form: FormStructure, test_scenarios: List[str]) -> List[TestResult]:
+ """
+ Führt umfassende Tests für ein einzelnes Formular durch.
+ """
+ results = []
+
+ # Test-Engines initialisieren
+ interaction_engine = FormInteractionEngine(self.page)
+ visual_validator = VisualFormValidator(self.page)
+
+ # Screenshots für Dokumentation
+ screenshots = await self.reporter.capture_form_states(
+ self.page, form.selector, f"form_test_{form.selector.replace('#', '').replace('.', '')}"
+ )
+
+ try:
+ # 1. Gültige Daten testen
+ if 'valid' in test_scenarios:
+ print(" ✅ Teste gültige Eingaben...")
+ valid_results = await self._test_valid_form_submission(form, interaction_engine)
+ results.extend(valid_results)
+
+ # 2. Ungültige Daten testen
+ if 'invalid' in test_scenarios:
+ print(" ❌ Teste ungültige Eingaben...")
+ invalid_results = await self._test_invalid_form_submission(form, interaction_engine)
+ results.extend(invalid_results)
+
+ # 3. Edge Cases testen
+ if 'edge_cases' in test_scenarios:
+ print(" 🔍 Teste Edge Cases...")
+ edge_results = await self._test_edge_cases(form, interaction_engine)
+ results.extend(edge_results)
+
+ # 4. Accessibility testen
+ if 'accessibility' in test_scenarios:
+ print(" ♿ Teste Accessibility...")
+ accessibility_results = await visual_validator.check_form_accessibility(form)
+ results.extend(accessibility_results)
+
+ # 5. Validierungen testen
+ if 'validations' in test_scenarios:
+ print(" 🔧 Teste Validierungen...")
+ validation_results = await interaction_engine.test_form_validations(form)
+ results.extend(validation_results)
+
+ # 6. Responsive Design testen
+ if 'responsive' in test_scenarios:
+ print(" 📱 Teste Responsive Design...")
+ responsive_results = await visual_validator.test_responsive_behavior(form)
+ results.extend(responsive_results)
+
+ # 7. Dynamisches Verhalten testen
+ if 'dynamic' in test_scenarios:
+ print(" ⚡ Teste dynamisches Verhalten...")
+ dynamic_results = await interaction_engine.test_dynamic_form_behavior(form)
+ results.extend(dynamic_results)
+
+ # 8. Error Display testen
+ if 'error_display' in test_scenarios:
+ print(" 🚨 Teste Error Display...")
+ error_results = await visual_validator.validate_error_display(form)
+ results.extend(error_results)
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type="comprehensive_test",
+ status=TestStatus.FAILED,
+ message=f"Fehler beim umfassenden Test: {str(e)}"
+ ))
+
+ # Screenshots zu Ergebnissen hinzufügen
+ for result in results:
+ if not result.screenshot_path and screenshots.get('form_focus'):
+ result.screenshot_path = screenshots['form_focus']
+
+ return results
+
+ async def _test_valid_form_submission(self, form: FormStructure, interaction_engine: FormInteractionEngine) -> List[TestResult]:
+ """Testet Formular mit gültigen Daten"""
+
+ # Gültige Test-Daten generieren
+ test_data = {}
+ for field in form.fields:
+ if field.field_type not in ['submit', 'button', 'hidden']:
+ test_data[field.name] = self.data_generator.generate_for_field(field, 'valid')
+
+ start_time = time.time()
+
+ # Formular ausfüllen
+ fill_results = await interaction_engine.fill_form_like_human(form, test_data)
+
+ # Versuche zu submitten
+ try:
+ if form.submit_button:
+ await self.page.click(form.submit_button)
+ await asyncio.sleep(2) # Warten auf Response
+
+ # Prüfe Erfolg (Page-Change, Success-Message, etc.)
+ success = await self._check_submission_success()
+
+ execution_time = time.time() - start_time
+
+ if success:
+ fill_results.append(TestResult(
+ form_selector=form.selector,
+ test_type="valid_submission",
+ status=TestStatus.PASSED,
+ message="Gültige Formular-Submission erfolgreich",
+ execution_time=execution_time,
+ details={'test_data': test_data}
+ ))
+ else:
+ fill_results.append(TestResult(
+ form_selector=form.selector,
+ test_type="valid_submission",
+ status=TestStatus.FAILED,
+ message="Gültige Submission wurde nicht akzeptiert",
+ execution_time=execution_time,
+ details={'test_data': test_data}
+ ))
+
+ except Exception as e:
+ fill_results.append(TestResult(
+ form_selector=form.selector,
+ test_type="valid_submission",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei gültiger Submission: {str(e)}",
+ execution_time=time.time() - start_time
+ ))
+
+ return fill_results
+
+ async def _test_invalid_form_submission(self, form: FormStructure, interaction_engine: FormInteractionEngine) -> List[TestResult]:
+ """Testet Formular mit ungültigen Daten"""
+
+ # Ungültige Test-Daten generieren
+ test_data = {}
+ for field in form.fields:
+ if field.field_type not in ['submit', 'button', 'hidden']:
+ test_data[field.name] = self.data_generator.generate_for_field(field, 'invalid')
+
+ start_time = time.time()
+
+ # Formular ausfüllen
+ fill_results = await interaction_engine.fill_form_like_human(form, test_data)
+
+ # Versuche zu submitten
+ try:
+ if form.submit_button:
+ await self.page.click(form.submit_button)
+ await asyncio.sleep(2)
+
+ # Prüfe ob Validierung greift (Submission sollte fehlschlagen)
+ validation_triggered = await self._check_validation_triggered()
+
+ execution_time = time.time() - start_time
+
+ if validation_triggered:
+ fill_results.append(TestResult(
+ form_selector=form.selector,
+ test_type="invalid_submission",
+ status=TestStatus.PASSED,
+ message="Ungültige Daten wurden korrekt abgewiesen",
+ execution_time=execution_time,
+ details={'test_data': test_data}
+ ))
+ else:
+ fill_results.append(TestResult(
+ form_selector=form.selector,
+ test_type="invalid_submission",
+ status=TestStatus.FAILED,
+ message="Ungültige Daten wurden fälschlicherweise akzeptiert",
+ execution_time=execution_time,
+ details={'test_data': test_data}
+ ))
+
+ except Exception as e:
+ fill_results.append(TestResult(
+ form_selector=form.selector,
+ test_type="invalid_submission",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei ungültiger Submission: {str(e)}",
+ execution_time=time.time() - start_time
+ ))
+
+ return fill_results
+
+ async def _test_edge_cases(self, form: FormStructure, interaction_engine: FormInteractionEngine) -> List[TestResult]:
+ """Testet Edge Cases wie XSS, SQL Injection, etc."""
+
+ results = []
+ edge_test_cases = [
+ "xss_injection",
+ "sql_injection",
+ "unicode_characters",
+ "extremely_long_input",
+ "null_bytes",
+ "whitespace_only"
+ ]
+
+ for test_case in edge_test_cases:
+ # Edge-Case Test-Daten generieren
+ test_data = {}
+ for field in form.fields:
+ if field.field_type not in ['submit', 'button', 'hidden']:
+ test_data[field.name] = self.data_generator.generate_for_field(field, 'edge_case')
+
+ try:
+ start_time = time.time()
+
+ # Formular ausfüllen
+ await interaction_engine.fill_form_like_human(form, test_data)
+
+ # Submit versuchen
+ if form.submit_button:
+ await self.page.click(form.submit_button)
+ await asyncio.sleep(1)
+
+ # Prüfe ob System stabil bleibt
+ page_crashed = await self._check_page_stability()
+
+ execution_time = time.time() - start_time
+
+ if not page_crashed:
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type=f"edge_case_{test_case}",
+ status=TestStatus.PASSED,
+ message=f"Edge Case '{test_case}' korrekt behandelt",
+ execution_time=execution_time,
+ details={'test_data': test_data}
+ ))
+ else:
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type=f"edge_case_{test_case}",
+ status=TestStatus.FAILED,
+ message=f"Edge Case '{test_case}' verursacht Probleme",
+ execution_time=execution_time,
+ details={'test_data': test_data}
+ ))
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector=form.selector,
+ test_type=f"edge_case_{test_case}",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Edge Case '{test_case}': {str(e)}"
+ ))
+
+ return results
+
+ async def _check_submission_success(self) -> bool:
+ """Prüft ob eine Formular-Submission erfolgreich war"""
+
+ try:
+ # Warten auf Navigation oder Änderungen
+ await asyncio.sleep(1)
+
+ success_indicators = await self.page.evaluate("""
+ () => {
+ // Verschiedene Erfolgs-Indikatoren prüfen
+ const successElements = document.querySelectorAll(
+ '.success, .alert-success, .message-success, .notification-success, ' +
+ '[class*="success"], [role="alert"][class*="success"]'
+ );
+
+ const errorElements = document.querySelectorAll(
+ '.error, .alert-error, .alert-danger, .message-error, ' +
+ '[class*="error"], [class*="danger"], [role="alert"][class*="error"]'
+ );
+
+ const urlChanged = window.location.href !== document.referrer;
+
+ return {
+ hasSuccessMessage: successElements.length > 0,
+ hasErrorMessage: errorElements.length > 0,
+ urlChanged: urlChanged,
+ currentUrl: window.location.href
+ };
+ }
+ """)
+
+ # Success wenn: Success-Message ODER URL-Änderung UND keine Error-Message
+ return ((success_indicators['hasSuccessMessage'] or success_indicators['urlChanged'])
+ and not success_indicators['hasErrorMessage'])
+
+ except Exception:
+ return False
+
+ async def _check_validation_triggered(self) -> bool:
+ """Prüft ob Client-Side-Validierung ausgelöst wurde"""
+
+ try:
+ validation_info = await self.page.evaluate("""
+ () => {
+ // HTML5 Validierung prüfen
+ const invalidFields = document.querySelectorAll(':invalid');
+
+ // Custom Validierungs-Elemente
+ const errorElements = document.querySelectorAll(
+ '.error, .invalid-feedback, .field-error, .validation-error, ' +
+ '[role="alert"], .alert-danger, .text-danger'
+ );
+
+ // Prüfe ob Submit verhindert wurde (URL ändert sich nicht)
+ const formElements = document.querySelectorAll('form');
+
+ return {
+ invalidFieldsCount: invalidFields.length,
+ errorElementsCount: errorElements.length,
+ hasValidationMessages: errorElements.length > 0
+ };
+ }
+ """)
+
+ return (validation_info['invalidFieldsCount'] > 0 or
+ validation_info['hasValidationMessages'])
+
+ except Exception:
+ return False
+
+ async def _check_page_stability(self) -> bool:
+ """Prüft ob die Seite nach Edge-Case-Input stabil bleibt"""
+
+ try:
+ # Warte kurz und prüfe dann
+ await asyncio.sleep(0.5)
+
+ # Prüfe ob Page noch reagiert
+ title = await self.page.title()
+
+ # Prüfe auf JavaScript-Fehler
+ errors = await self.page.evaluate("""
+ () => {
+ return window.testErrors || [];
+ }
+ """)
+
+ # Stabil wenn Title abgerufen werden kann und keine kritischen Fehler
+ return title is not None and len(errors) == 0
+
+ except Exception:
+ return False # Page crashed
+
+ async def test_form(self, url: str, form_selector: str, test_data: Dict[str, str] = None,
+ test_scenarios: List[str] = None) -> List[TestResult]:
+ """
+ Testet ein spezifisches Formular mit benutzerdefinierten Daten.
+
+ Args:
+ url: URL der Seite mit dem Formular
+ form_selector: CSS-Selektor für das Formular
+ test_data: Benutzerdefinierte Test-Daten
+ test_scenarios: Test-Szenarien ['valid', 'invalid', 'edge_cases']
+ """
+ if test_scenarios is None:
+ test_scenarios = ['valid', 'invalid']
+
+ # Seite laden
+ full_url = url if url.startswith('http') else f"{self.base_url}{url}"
+ await self.page.goto(full_url, wait_until="networkidle")
+
+ # Formular finden
+ forms = await self.scan_for_forms(url)
+ target_form = None
+
+ for form in forms:
+ if form.selector == form_selector:
+ target_form = form
+ break
+
+ if not target_form:
+ return [TestResult(
+ form_selector=form_selector,
+ test_type="form_discovery",
+ status=TestStatus.FAILED,
+ message=f"Formular mit Selektor '{form_selector}' nicht gefunden"
+ )]
+
+ # Mit benutzerdefinierten Daten überschreiben falls vorhanden
+ if test_data:
+ interaction_engine = FormInteractionEngine(self.page)
+
+ start_time = time.time()
+ results = await interaction_engine.fill_form_like_human(target_form, test_data)
+
+ # Submit
+ if target_form.submit_button:
+ await self.page.click(target_form.submit_button)
+ await asyncio.sleep(2)
+
+ success = await self._check_submission_success()
+ execution_time = time.time() - start_time
+
+ results.append(TestResult(
+ form_selector=form_selector,
+ test_type="custom_data_submission",
+ status=TestStatus.PASSED if success else TestStatus.FAILED,
+ message="Custom Test-Daten submission " + ("erfolgreich" if success else "fehlgeschlagen"),
+ execution_time=execution_time,
+ details={'test_data': test_data}
+ ))
+
+ self.test_results.extend(results)
+ return results
+
+ # Standard-Tests durchführen
+ return await self.test_form_comprehensive(target_form, test_scenarios)
+
+ async def test_multi_step_form(self, start_url: str, steps: List[str]) -> List[TestResult]:
+ """
+ Testet mehrstufige Formulare.
+
+ Args:
+ start_url: URL des ersten Schritts
+ steps: Liste der Schritt-Selektoren oder -URLs
+ """
+ results = []
+
+ try:
+ # Erster Schritt
+ full_url = start_url if start_url.startswith('http') else f"{self.base_url}{start_url}"
+ await self.page.goto(full_url, wait_until="networkidle")
+
+ current_step = 1
+
+ for step_identifier in steps:
+ print(f"🔄 Teste Multi-Step Schritt {current_step}: {step_identifier}")
+
+ # Formulare im aktuellen Schritt finden
+ forms = await self.scan_for_forms(self.page.url)
+
+ if forms:
+ # Erstes Formular im Schritt testen
+ step_form = forms[0]
+
+ # Test-Daten generieren
+ test_data = {}
+ for field in step_form.fields:
+ if field.field_type not in ['submit', 'button', 'hidden']:
+ test_data[field.name] = self.data_generator.generate_for_field(field, 'valid')
+
+ # Schritt ausfüllen
+ interaction_engine = FormInteractionEngine(self.page)
+ fill_results = await interaction_engine.fill_form_like_human(step_form, test_data)
+ results.extend(fill_results)
+
+ # Zum nächsten Schritt
+ if step_form.submit_button:
+ await self.page.click(step_form.submit_button)
+ await asyncio.sleep(2)
+
+ # Prüfe ob nächster Schritt erreicht
+ if current_step < len(steps):
+ # Warte auf Navigation/Update
+ await asyncio.sleep(1)
+
+ results.append(TestResult(
+ form_selector=step_form.selector,
+ test_type=f"multi_step_navigation_{current_step}",
+ status=TestStatus.PASSED,
+ message=f"Navigation zu Schritt {current_step + 1} erfolgreich",
+ details={'step': current_step, 'test_data': test_data}
+ ))
+ else:
+ # Finaler Schritt - prüfe Erfolg
+ success = await self._check_submission_success()
+
+ results.append(TestResult(
+ form_selector=step_form.selector,
+ test_type="multi_step_completion",
+ status=TestStatus.PASSED if success else TestStatus.FAILED,
+ message="Multi-Step Formular " + ("erfolgreich abgeschlossen" if success else "Abschluss fehlgeschlagen"),
+ details={'total_steps': len(steps)}
+ ))
+
+ current_step += 1
+
+ except Exception as e:
+ results.append(TestResult(
+ form_selector="multi_step_form",
+ test_type="multi_step_error",
+ status=TestStatus.FAILED,
+ message=f"Fehler bei Multi-Step Test: {str(e)}"
+ ))
+
+ self.test_results.extend(results)
+ return results
+
+ async def test_form_on_devices(self, url: str, devices: List[str] = None) -> List[TestResult]:
+ """
+ Testet Formulare auf verschiedenen Geräte-Viewports.
+
+ Args:
+ url: URL der zu testenden Seite
+ devices: Liste der Geräte-Namen ['iPhone 12', 'iPad', 'Desktop 1920x1080']
+ """
+ if devices is None:
+ devices = ['iPhone SE', 'iPad', 'Desktop 1920x1080']
+
+ device_viewports = {
+ 'iPhone SE': {'width': 375, 'height': 667},
+ 'iPhone 12': {'width': 390, 'height': 844},
+ 'iPad': {'width': 768, 'height': 1024},
+ 'Desktop 1920x1080': {'width': 1920, 'height': 1080},
+ 'Desktop 1280x720': {'width': 1280, 'height': 720}
+ }
+
+ all_results = []
+ original_viewport = self.page.viewport_size
+
+ for device in devices:
+ if device in device_viewports:
+ viewport = device_viewports[device]
+
+ print(f"📱 Teste auf {device} ({viewport['width']}x{viewport['height']})")
+
+ # Viewport setzen
+ await self.page.set_viewport_size(width=viewport['width'], height=viewport['height'])
+ await asyncio.sleep(1)
+
+ # Seite neu laden für korrekte Responsive-Darstellung
+ full_url = url if url.startswith('http') else f"{self.base_url}{url}"
+ await self.page.goto(full_url, wait_until="networkidle")
+
+ # Formulare testen
+ forms = await self.scan_for_forms(url)
+
+ for form in forms:
+ visual_validator = VisualFormValidator(self.page)
+ responsive_results = await visual_validator.test_responsive_behavior(
+ form, [{'width': viewport['width'], 'height': viewport['height'], 'name': device}]
+ )
+
+ # Screenshots für Device-Tests
+ device_screenshots = await self.reporter.capture_form_states(
+ self.page, form.selector, f"device_test_{device.replace(' ', '_').lower()}"
+ )
+
+ for result in responsive_results:
+ if device_screenshots.get('form_focus'):
+ result.screenshot_path = device_screenshots['form_focus']
+
+ all_results.extend(responsive_results)
+
+ # Ursprüngliches Viewport wiederherstellen
+ if original_viewport:
+ await self.page.set_viewport_size(
+ width=original_viewport['width'],
+ height=original_viewport['height']
+ )
+
+ self.test_results.extend(all_results)
+ return all_results
+
+ def generate_report(self, report_name: str = "form_test_report") -> str:
+ """
+ Generiert einen umfassenden Test-Report.
+
+ Args:
+ report_name: Name des Reports
+
+ Returns:
+ Pfad zum generierten HTML-Report
+ """
+ if not self.test_results:
+ print("⚠️ Keine Test-Ergebnisse vorhanden")
+ return ""
+
+ # HTML-Report generieren
+ html_report_path = self.reporter.generate_visual_report(self.test_results, report_name)
+
+ # JSON-Report für maschinelle Verarbeitung
+ json_report_path = self.reporter.generate_json_report(self.test_results, report_name)
+
+ # Zusammenfassung in Console
+ self._print_test_summary()
+
+ return html_report_path
+
+ def _print_test_summary(self):
+ """Druckt eine Zusammenfassung der Test-Ergebnisse"""
+
+ if not self.test_results:
+ return
+
+ total = len(self.test_results)
+ passed = len([r for r in self.test_results if r.status == TestStatus.PASSED])
+ failed = len([r for r in self.test_results if r.status == TestStatus.FAILED])
+ warnings = len([r for r in self.test_results if r.status == TestStatus.WARNING])
+ skipped = len([r for r in self.test_results if r.status == TestStatus.SKIPPED])
+
+ success_rate = (passed / total * 100) if total > 0 else 0
+
+ if RICH_AVAILABLE and console:
+ # Rich Table für schöne Ausgabe
+ table = Table(title="🧪 Formular Test Zusammenfassung")
+ table.add_column("Metrik", style="bold")
+ table.add_column("Anzahl", justify="right")
+ table.add_column("Prozent", justify="right")
+
+ table.add_row("Gesamt Tests", str(total), "100.0%")
+ table.add_row("✅ Bestanden", str(passed), f"{passed/total*100:.1f}%", style="green")
+ table.add_row("❌ Fehlgeschlagen", str(failed), f"{failed/total*100:.1f}%", style="red")
+ table.add_row("⚠️ Warnungen", str(warnings), f"{warnings/total*100:.1f}%", style="yellow")
+ table.add_row("⏭️ Übersprungen", str(skipped), f"{skipped/total*100:.1f}%", style="blue")
+ table.add_row("", "", "", style="dim")
+ table.add_row("🎯 Erfolgsrate", f"{success_rate:.1f}%", "",
+ style="green" if success_rate >= 80 else "yellow" if success_rate >= 60 else "red")
+
+ console.print("\n")
+ console.print(table)
+ console.print("\n")
+
+ # Status-basierte Ausgabe
+ if success_rate >= 90:
+ console.print("🎉 Exzellente Test-Ergebnisse!", style="bold green")
+ elif success_rate >= 80:
+ console.print("✅ Gute Test-Ergebnisse!", style="bold yellow")
+ elif success_rate >= 60:
+ console.print("⚠️ Verbesserungsbedarf bei den Formularen", style="bold orange")
+ else:
+ console.print("❌ Kritische Probleme gefunden!", style="bold red")
+ else:
+ # Einfache Text-Ausgabe
+ print(f"""
+{'='*60}
+🧪 FORMULAR TEST ZUSAMMENFASSUNG
+{'='*60}
+
+📊 STATISTIKEN:
+ Gesamt Tests: {total}
+ ✅ Bestanden: {passed} ({passed/total*100:.1f}%)
+ ❌ Fehlgeschlagen: {failed} ({failed/total*100:.1f}%)
+ ⚠️ Warnungen: {warnings} ({warnings/total*100:.1f}%)
+ ⏭️ Übersprungen: {skipped} ({skipped/total*100:.1f}%)
+
+🎯 ERFOLGSRATE: {success_rate:.1f}%
+
+{'='*60}
+""")
+
+
+# CLI-Interface für den Form Test Automator
+async def main():
+ """Haupt-CLI-Interface für das Tool"""
+
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ description="Flask HTML-Formular Test Automator",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+BEISPIELE:
+ # Alle Formulare auf einer Seite testen
+ python form_test_automator.py --url http://localhost:5000/register --test-all
+
+ # Spezifisches Formular testen
+ python form_test_automator.py --url http://localhost:5000/contact --form "#contact-form"
+
+ # Multi-Step Formular testen
+ python form_test_automator.py --url http://localhost:5000/signup --multi-step --steps "step1,step2,step3"
+
+ # Responsive Testing
+ python form_test_automator.py --url http://localhost:5000/order --responsive --devices "iPhone 12,iPad,Desktop"
+
+ # Umfassende Tests mit allen Szenarien
+ python form_test_automator.py --url http://localhost:5000/application --comprehensive
+ """
+ )
+
+ parser.add_argument('--url', required=True, help='Basis-URL der Flask-Anwendung')
+ parser.add_argument('--form', help='CSS-Selektor für spezifisches Formular')
+ parser.add_argument('--test-all', action='store_true', help='Teste alle Formulare auf der Seite')
+ parser.add_argument('--multi-step', action='store_true', help='Multi-Step Formular testen')
+ parser.add_argument('--steps', help='Komma-getrennte Liste der Schritte (für Multi-Step)')
+ parser.add_argument('--responsive', action='store_true', help='Responsive Design testen')
+ parser.add_argument('--devices', help='Komma-getrennte Liste der Test-Devices')
+ parser.add_argument('--comprehensive', action='store_true', help='Umfassende Tests mit allen Szenarien')
+ parser.add_argument('--scenarios', help='Komma-getrennte Test-Szenarien (valid,invalid,edge_cases,accessibility,etc.)')
+ parser.add_argument('--browser', choices=['chromium', 'firefox', 'webkit'], default='chromium', help='Browser-Engine')
+ parser.add_argument('--headless', action='store_true', default=True, help='Headless-Modus (Standard: True)')
+ parser.add_argument('--headed', action='store_true', help='Browser-Fenster anzeigen (Gegenteil von headless)')
+ parser.add_argument('--output', help='Ausgabeverzeichnis für Reports')
+ parser.add_argument('--report-name', default='form_test_report', help='Name des generierten Reports')
+
+ args = parser.parse_args()
+
+ # Headless-Modus bestimmen
+ headless = args.headless and not args.headed
+
+ # Output-Verzeichnis
+ output_dir = Path(args.output) if args.output else None
+
+ # Test-Szenarien
+ if args.comprehensive:
+ test_scenarios = ['valid', 'invalid', 'edge_cases', 'accessibility', 'validations', 'responsive', 'dynamic', 'error_display']
+ elif args.scenarios:
+ test_scenarios = [s.strip() for s in args.scenarios.split(',')]
+ else:
+ test_scenarios = ['valid', 'invalid', 'accessibility']
+
+ print(f"""
+{'='*70}
+🧪 FLASK HTML-FORMULAR TEST AUTOMATOR
+{'='*70}
+
+🎯 Target URL: {args.url}
+🌐 Browser: {args.browser} ({'headless' if headless else 'headed'})
+📋 Test-Szenarien: {', '.join(test_scenarios)}
+📁 Output: {output_dir or 'form_test_reports/'}
+
+{'='*70}
+""")
+
+ try:
+ # Test Automator initialisieren
+ async with HTMLFormTestAutomator(
+ base_url=args.url,
+ browser=args.browser,
+ headless=headless
+ ) as automator:
+
+ if output_dir:
+ automator.reporter.output_dir = output_dir
+
+ if args.test_all:
+ # Alle Formulare auf der Seite testen
+ results = await automator.test_all_forms_on_page('/', test_scenarios)
+
+ elif args.form:
+ # Spezifisches Formular testen
+ results = await automator.test_form('/', args.form, test_scenarios=test_scenarios)
+
+ elif args.multi_step and args.steps:
+ # Multi-Step Formular testen
+ steps = [s.strip() for s in args.steps.split(',')]
+ results = await automator.test_multi_step_form('/', steps)
+
+ elif args.responsive:
+ # Responsive Testing
+ devices = ['iPhone 12', 'iPad', 'Desktop 1920x1080']
+ if args.devices:
+ devices = [d.strip() for d in args.devices.split(',')]
+
+ results = await automator.test_form_on_devices('/', devices)
+
+ else:
+ # Standard: Alle Formulare mit Standard-Szenarien
+ results = await automator.test_all_forms_on_page('/', test_scenarios)
+
+ # Report generieren
+ report_path = automator.generate_report(args.report_name)
+
+ if report_path:
+ print(f"\n🎉 Test abgeschlossen! Report verfügbar: {report_path}")
+ else:
+ print("\n⚠️ Test abgeschlossen, aber Report-Generierung fehlgeschlagen")
+
+ except KeyboardInterrupt:
+ print("\n⚠️ Test durch Benutzer abgebrochen")
+ except Exception as e:
+ print(f"\n❌ Fehler beim Test: {str(e)}")
+ import traceback
+ traceback.print_exc()
+
+
+if __name__ == "__main__":
+ if not PLAYWRIGHT_AVAILABLE:
+ print("""
+❌ PLAYWRIGHT NICHT VERFÜGBAR
+
+Installiere Playwright mit:
+ pip install playwright
+ playwright install chromium
+
+Oder installiere alle Dependencies:
+ pip install playwright faker beautifulsoup4 rich
+ playwright install
+ """)
+ exit(1)
+
+ asyncio.run(main())
\ No newline at end of file
diff --git a/backend/form_tester_setup.sh b/backend/form_tester_setup.sh
new file mode 100644
index 000000000..89a964815
--- /dev/null
+++ b/backend/form_tester_setup.sh
@@ -0,0 +1,223 @@
+#!/bin/bash
+# Flask HTML-Formular Test Automator - Setup Skript
+# =================================================
+
+echo "🧪 Flask HTML-Formular Test Automator Setup"
+echo "============================================"
+
+# Farben für Ausgabe
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Funktionen
+print_status() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+print_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+print_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Python-Version prüfen
+print_status "Prüfe Python-Version..."
+python_version=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
+required_version="3.8"
+
+if python3 -c "import sys; exit(0 if sys.version_info >= (3, 8) else 1)"; then
+ print_success "Python $python_version ist kompatibel (benötigt: $required_version+)"
+else
+ print_error "Python $required_version oder höher erforderlich (gefunden: $python_version)"
+ exit 1
+fi
+
+# Virtual Environment erstellen (optional)
+if [ "$1" = "--venv" ]; then
+ print_status "Erstelle Virtual Environment..."
+ python3 -m venv form_tester_env
+ source form_tester_env/bin/activate
+ print_success "Virtual Environment aktiviert"
+fi
+
+# Python-Dependencies installieren
+print_status "Installiere Python-Dependencies..."
+if [ -f "requirements_form_tester.txt" ]; then
+ if python3 -m pip install -r requirements_form_tester.txt; then
+ print_success "Python-Dependencies installiert"
+ else
+ print_error "Fehler beim Installieren der Python-Dependencies"
+ exit 1
+ fi
+else
+ print_warning "requirements_form_tester.txt nicht gefunden, installiere Basis-Dependencies..."
+ python3 -m pip install playwright faker beautifulsoup4 rich
+fi
+
+# Playwright Browser installieren
+print_status "Installiere Playwright Browser..."
+if python3 -m playwright install chromium firefox webkit; then
+ print_success "Playwright Browser installiert"
+else
+ print_error "Fehler beim Installieren der Playwright Browser"
+ exit 1
+fi
+
+# Zusätzliche Systemabhängigkeiten für Linux
+if [[ "$OSTYPE" == "linux-gnu"* ]]; then
+ print_status "Installiere System-Dependencies für Linux..."
+
+ # Detect package manager
+ if command -v apt-get &> /dev/null; then
+ sudo apt-get update
+ sudo apt-get install -y \
+ libnss3 \
+ libnspr4 \
+ libatk-bridge2.0-0 \
+ libdrm2 \
+ libxkbcommon0 \
+ libgtk-3-0 \
+ libatspi2.0-0 \
+ libx11-xcb1 \
+ libxcomposite1 \
+ libxdamage1 \
+ libxrandr2 \
+ libgbm1 \
+ libxss1 \
+ libasound2
+ print_success "System-Dependencies installiert"
+ elif command -v yum &> /dev/null; then
+ sudo yum install -y \
+ nss \
+ nspr \
+ at-spi2-atk \
+ libdrm \
+ libxkbcommon \
+ gtk3 \
+ at-spi2-core \
+ libXcomposite \
+ libXdamage \
+ libXrandr \
+ mesa-libgbm \
+ libXScrnSaver \
+ alsa-lib
+ print_success "System-Dependencies installiert"
+ else
+ print_warning "Unbekannter Package Manager, überspringe System-Dependencies"
+ fi
+fi
+
+# Test-Verzeichnis erstellen
+print_status "Erstelle Test-Verzeichnisse..."
+mkdir -p form_test_reports
+mkdir -p form_test_reports/screenshots
+mkdir -p form_test_reports/videos
+mkdir -p form_test_reports/reports
+print_success "Test-Verzeichnisse erstellt"
+
+# Ausführungsrechte setzen
+print_status "Setze Ausführungsrechte..."
+chmod +x form_test_automator.py
+chmod +x test_forms_example.py
+print_success "Ausführungsrechte gesetzt"
+
+# Verfügbarkeit testen
+print_status "Teste Installation..."
+if python3 -c "from playwright.async_api import async_playwright; print('Playwright OK')"; then
+ print_success "Playwright erfolgreich installiert"
+else
+ print_error "Playwright-Test fehlgeschlagen"
+ exit 1
+fi
+
+if python3 -c "from faker import Faker; print('Faker OK')"; then
+ print_success "Faker erfolgreich installiert"
+else
+ print_warning "Faker nicht verfügbar (optional)"
+fi
+
+if python3 -c "from bs4 import BeautifulSoup; print('BeautifulSoup OK')"; then
+ print_success "BeautifulSoup erfolgreich installiert"
+else
+ print_warning "BeautifulSoup nicht verfügbar (optional)"
+fi
+
+if python3 -c "from rich.console import Console; print('Rich OK')"; then
+ print_success "Rich erfolgreich installiert"
+else
+ print_warning "Rich nicht verfügbar (optional)"
+fi
+
+# Beispiel-Konfiguration erstellen
+print_status "Erstelle Beispiel-Konfiguration..."
+cat > form_tester_config.json << EOF
+{
+ "base_url": "http://localhost:5000",
+ "browser": "chromium",
+ "headless": true,
+ "viewport": {
+ "width": 1280,
+ "height": 720
+ },
+ "default_scenarios": [
+ "valid",
+ "invalid",
+ "accessibility"
+ ],
+ "test_devices": [
+ "iPhone SE",
+ "iPhone 12",
+ "iPad",
+ "Desktop 1280x720"
+ ],
+ "output_directory": "form_test_reports",
+ "screenshot_on_failure": true,
+ "video_recording": false
+}
+EOF
+print_success "Beispiel-Konfiguration erstellt: form_tester_config.json"
+
+# Erfolgreiche Installation
+echo ""
+echo "🎉 Installation erfolgreich abgeschlossen!"
+echo "========================================="
+echo ""
+echo "📋 NÄCHSTE SCHRITTE:"
+echo ""
+echo "1. Flask-App starten:"
+echo " python app.py"
+echo ""
+echo "2. Formular-Tests ausführen:"
+echo " python form_test_automator.py --url http://localhost:5000 --test-all"
+echo ""
+echo "3. MYP-spezifische Tests:"
+echo " python test_forms_example.py"
+echo ""
+echo "4. CLI-Hilfe anzeigen:"
+echo " python form_test_automator.py --help"
+echo ""
+echo "📁 AUSGABE-VERZEICHNISSE:"
+echo " • Screenshots: form_test_reports/screenshots/"
+echo " • Videos: form_test_reports/videos/"
+echo " • Reports: form_test_reports/reports/"
+echo ""
+echo "🔧 KONFIGURATION:"
+echo " • Anpassen in: form_tester_config.json"
+echo ""
+
+if [ "$1" = "--venv" ]; then
+ echo "💡 Virtual Environment aktiviert. Zum Deaktivieren:"
+ echo " deactivate"
+fi
+
+echo ""
+echo "🧪 Flask HTML-Formular Test Automator bereit!"
\ No newline at end of file
diff --git a/backend/requirements_form_tester.txt b/backend/requirements_form_tester.txt
new file mode 100644
index 000000000..445672e5e
--- /dev/null
+++ b/backend/requirements_form_tester.txt
@@ -0,0 +1,37 @@
+# Flask HTML-Formular Test Automator - Dependencies
+# =================================================
+
+# Browser-Automation (Haupt-Abhängigkeit)
+playwright>=1.40.0
+
+# Test-Daten-Generierung
+faker>=20.1.0
+
+# HTML-Parsing
+beautifulsoup4>=4.12.0
+
+# Rich Console-Ausgabe (optional, aber empfohlen)
+rich>=13.7.0
+
+# Zusätzliche nützliche Libraries
+# --------------------------------
+
+# Für erweiterte Bild-Verarbeitung (optional)
+# Pillow>=10.1.0
+
+# Für JSON-Schema-Validierung (optional)
+# jsonschema>=4.20.0
+
+# Für HTTP-Requests außerhalb von Playwright (optional)
+# requests>=2.31.0
+
+# Für Datenbank-Tests (optional)
+# sqlalchemy>=2.0.0
+
+# Für CSV/Excel-Export (optional)
+# pandas>=2.1.0
+# openpyxl>=3.1.0
+
+# Installation:
+# pip install -r requirements_form_tester.txt
+# playwright install chromium firefox webkit
\ No newline at end of file
diff --git a/backend/test_forms_example.py b/backend/test_forms_example.py
new file mode 100644
index 000000000..ede2fbebb
--- /dev/null
+++ b/backend/test_forms_example.py
@@ -0,0 +1,555 @@
+#!/usr/bin/env python3
+"""
+Beispiel-Skript für den Flask HTML-Formular Test Automator
+=========================================================
+
+Dieses Skript demonstriert verschiedene Anwendungsfälle des Form Test Automators
+speziell für das MYP (Manage Your Printers) System.
+
+Autor: Till Tomczak
+Mercedes-Benz Projektarbeit MYP
+"""
+
+import asyncio
+import sys
+from pathlib import Path
+
+# Form Test Automator importieren
+try:
+ from form_test_automator import HTMLFormTestAutomator, TestStatus
+except ImportError:
+ print("❌ Kann form_test_automator nicht importieren. Stelle sicher, dass die Datei im gleichen Verzeichnis ist.")
+ sys.exit(1)
+
+
+async def test_myp_login_form():
+ """
+ Testet das MYP Login-Formular umfassend.
+ """
+ print("🔐 Teste MYP Login-Formular...")
+
+ async with HTMLFormTestAutomator(
+ base_url="http://localhost:5000",
+ browser="chromium",
+ headless=True
+ ) as automator:
+
+ # 1. Login-Formular mit gültigen Daten testen
+ valid_results = await automator.test_form(
+ url="/login",
+ form_selector="#login-form",
+ test_data={
+ "username": "admin",
+ "password": "admin123"
+ },
+ test_scenarios=["valid"]
+ )
+
+ # 2. Ungültige Login-Versuche testen
+ invalid_results = await automator.test_form(
+ url="/login",
+ form_selector="#login-form",
+ test_data={
+ "username": "wronguser",
+ "password": "wrongpass"
+ },
+ test_scenarios=["invalid"]
+ )
+
+ # 3. Accessibility und Responsive Design
+ accessibility_results = await automator.test_form(
+ url="/login",
+ form_selector="#login-form",
+ test_scenarios=["accessibility", "responsive"]
+ )
+
+ # Report generieren
+ report_path = automator.generate_report("myp_login_test")
+ print(f"📊 Login-Test Report: {report_path}")
+
+ return valid_results + invalid_results + accessibility_results
+
+
+async def test_myp_printer_registration():
+ """
+ Testet das Drucker-Registrierungs-Formular.
+ """
+ print("🖨️ Teste Drucker-Registrierung...")
+
+ async with HTMLFormTestAutomator(
+ base_url="http://localhost:5000",
+ browser="chromium",
+ headless=True
+ ) as automator:
+
+ # Verschiedene Drucker-Konfigurationen testen
+ printer_configs = [
+ {
+ "name": "Prusa i3 MK3S+",
+ "ip_address": "192.168.1.100",
+ "tapo_ip": "192.168.1.200",
+ "location": "Werkstatt A",
+ "description": "Hochpräzisionsdrucker für Prototypen"
+ },
+ {
+ "name": "Ender 3 Pro",
+ "ip_address": "192.168.1.101",
+ "tapo_ip": "192.168.1.201",
+ "location": "Werkstatt B",
+ "description": "Robuster Drucker für größere Teile"
+ }
+ ]
+
+ all_results = []
+
+ for config in printer_configs:
+ print(f" 📋 Teste Konfiguration: {config['name']}")
+
+ results = await automator.test_form(
+ url="/admin/add_printer",
+ form_selector="#add-printer-form",
+ test_data=config,
+ test_scenarios=["valid", "validations"]
+ )
+ all_results.extend(results)
+
+ # Edge Cases für IP-Adressen testen
+ edge_cases = [
+ {"ip_address": "999.999.999.999"}, # Ungültige IP
+ {"ip_address": "192.168.1"}, # Unvollständige IP
+ {"ip_address": "localhost"}, # Hostname statt IP
+ {"name": ""}, # Leerer Name
+ {"name": "x" * 500} # Zu langer Name
+ ]
+
+ for edge_case in edge_cases:
+ print(f" 🔍 Teste Edge Case: {edge_case}")
+
+ edge_results = await automator.test_form(
+ url="/admin/add_printer",
+ form_selector="#add-printer-form",
+ test_data=edge_case,
+ test_scenarios=["edge_cases"]
+ )
+ all_results.extend(edge_results)
+
+ # Report generieren
+ report_path = automator.generate_report("myp_printer_registration_test")
+ print(f"📊 Drucker-Registrierung Report: {report_path}")
+
+ return all_results
+
+
+async def test_myp_job_submission():
+ """
+ Testet das Druckauftrags-Formular.
+ """
+ print("📋 Teste Druckauftrags-Formular...")
+
+ async with HTMLFormTestAutomator(
+ base_url="http://localhost:5000",
+ browser="chromium",
+ headless=True
+ ) as automator:
+
+ # Standard-Druckauftrag testen
+ job_data = {
+ "title": "Prototyp Gehäuse v2.1",
+ "description": "Gehäuse für neuen Sensor, benötigt hohe Präzision",
+ "priority": "high",
+ "material": "PLA",
+ "infill": "20",
+ "layer_height": "0.2",
+ "estimated_time": "4:30",
+ "notes": "Bitte mit Support-Strukturen drucken"
+ }
+
+ # File-Upload simulieren (falls unterstützt)
+ # job_data["file"] = "test_models/gehaeuse_v2.stl"
+
+ results = await automator.test_form(
+ url="/new_job",
+ form_selector="#job-form",
+ test_data=job_data,
+ test_scenarios=["valid", "validations", "dynamic"]
+ )
+
+ # Verschiedene Prioritäten testen
+ priorities = ["urgent", "high", "normal", "low"]
+
+ for priority in priorities:
+ print(f" ⚡ Teste Priorität: {priority}")
+
+ priority_test_data = job_data.copy()
+ priority_test_data["priority"] = priority
+
+ priority_results = await automator.test_form(
+ url="/new_job",
+ form_selector="#job-form",
+ test_data=priority_test_data,
+ test_scenarios=["valid"]
+ )
+ results.extend(priority_results)
+
+ # Report generieren
+ report_path = automator.generate_report("myp_job_submission_test")
+ print(f"📊 Druckauftrags Report: {report_path}")
+
+ return results
+
+
+async def test_myp_guest_access():
+ """
+ Testet das Gast-Zugangs-System (OTP-basiert).
+ """
+ print("👤 Teste Gast-Zugang...")
+
+ async with HTMLFormTestAutomator(
+ base_url="http://localhost:5000",
+ browser="chromium",
+ headless=True
+ ) as automator:
+
+ # 1. Gast-Anfrage erstellen
+ guest_request_data = {
+ "name": "Max Mustermann",
+ "email": "max.mustermann@mercedes-benz.com",
+ "department": "Entwicklung",
+ "reason": "Prototyp für neues Projekt benötigt",
+ "requested_time": "2024-06-20T14:00"
+ }
+
+ request_results = await automator.test_form(
+ url="/guest_request",
+ form_selector="#guest-request-form",
+ test_data=guest_request_data,
+ test_scenarios=["valid", "validations"]
+ )
+
+ # 2. OTP-Eingabe simulieren (falls OTP bekannt)
+ # otp_results = await automator.test_form(
+ # url="/guest/otp",
+ # form_selector="#otp-form",
+ # test_data={"otp": "123456"},
+ # test_scenarios=["valid", "invalid"]
+ # )
+
+ # 3. Verschiedene Email-Formate testen
+ email_variants = [
+ "test@mercedes-benz.com",
+ "test.user@daimler.com",
+ "invalid-email", # Ungültig
+ "test@external.com", # Externes Domain
+ "" # Leer
+ ]
+
+ for email in email_variants:
+ print(f" 📧 Teste Email: {email}")
+
+ email_test_data = guest_request_data.copy()
+ email_test_data["email"] = email
+
+ email_results = await automator.test_form(
+ url="/guest_request",
+ form_selector="#guest-request-form",
+ test_data=email_test_data,
+ test_scenarios=["validations"]
+ )
+ request_results.extend(email_results)
+
+ # Report generieren
+ report_path = automator.generate_report("myp_guest_access_test")
+ print(f"📊 Gast-Zugang Report: {report_path}")
+
+ return request_results
+
+
+async def test_myp_settings_forms():
+ """
+ Testet verschiedene Einstellungs-Formulare.
+ """
+ print("⚙️ Teste Einstellungs-Formulare...")
+
+ async with HTMLFormTestAutomator(
+ base_url="http://localhost:5000",
+ browser="chromium",
+ headless=True
+ ) as automator:
+
+ all_results = []
+
+ # 1. Benutzer-Profil-Einstellungen
+ profile_data = {
+ "display_name": "Till Tomczak",
+ "email": "till.tomczak@mercedes-benz.com",
+ "notification_email": "true",
+ "notification_browser": "true",
+ "language": "de",
+ "timezone": "Europe/Berlin"
+ }
+
+ profile_results = await automator.test_form(
+ url="/profile",
+ form_selector="#profile-form",
+ test_data=profile_data,
+ test_scenarios=["valid", "validations"]
+ )
+ all_results.extend(profile_results)
+
+ # 2. System-Einstellungen (Admin)
+ system_data = {
+ "max_concurrent_jobs": "3",
+ "default_job_timeout": "480",
+ "auto_cleanup_days": "30",
+ "maintenance_mode": "false",
+ "email_notifications": "true",
+ "log_level": "INFO"
+ }
+
+ system_results = await automator.test_form(
+ url="/admin/settings",
+ form_selector="#system-settings-form",
+ test_data=system_data,
+ test_scenarios=["valid", "edge_cases"]
+ )
+ all_results.extend(system_results)
+
+ # 3. Tapo-Controller-Einstellungen
+ tapo_data = {
+ "device_ip": "192.168.1.200",
+ "username": "admin",
+ "password": "tapo_password",
+ "auto_power_on": "true",
+ "auto_power_off": "true",
+ "power_off_delay": "300"
+ }
+
+ tapo_results = await automator.test_form(
+ url="/admin/tapo_settings",
+ form_selector="#tapo-form",
+ test_data=tapo_data,
+ test_scenarios=["valid", "validations"]
+ )
+ all_results.extend(tapo_results)
+
+ # Report generieren
+ report_path = automator.generate_report("myp_settings_test")
+ print(f"📊 Einstellungen Report: {report_path}")
+
+ return all_results
+
+
+async def test_myp_responsive_design():
+ """
+ Testet alle wichtigen Formulare auf verschiedenen Geräten.
+ """
+ print("📱 Teste Responsive Design...")
+
+ async with HTMLFormTestAutomator(
+ base_url="http://localhost:5000",
+ browser="chromium",
+ headless=True
+ ) as automator:
+
+ # Wichtige Formulare für Responsive Tests
+ important_forms = [
+ "/login",
+ "/new_job",
+ "/guest_request",
+ "/profile"
+ ]
+
+ # Test-Geräte
+ devices = [
+ "iPhone SE", # Kleine Mobile
+ "iPhone 12", # Standard Mobile
+ "iPad", # Tablet
+ "Desktop 1280x720", # Standard Desktop
+ "Desktop 1920x1080" # Large Desktop
+ ]
+
+ all_results = []
+
+ for url in important_forms:
+ print(f" 📋 Teste {url} auf allen Geräten...")
+
+ responsive_results = await automator.test_form_on_devices(url, devices)
+ all_results.extend(responsive_results)
+
+ # Report generieren
+ report_path = automator.generate_report("myp_responsive_test")
+ print(f"📊 Responsive Design Report: {report_path}")
+
+ return all_results
+
+
+async def test_myp_accessibility():
+ """
+ Führt umfassende Accessibility-Tests für alle MYP-Formulare durch.
+ """
+ print("♿ Teste Accessibility...")
+
+ async with HTMLFormTestAutomator(
+ base_url="http://localhost:5000",
+ browser="chromium",
+ headless=True
+ ) as automator:
+
+ # Alle wichtigen Seiten für Accessibility
+ pages_to_test = [
+ "/",
+ "/login",
+ "/dashboard",
+ "/new_job",
+ "/printers",
+ "/guest_request",
+ "/profile",
+ "/admin"
+ ]
+
+ all_results = []
+
+ for page in pages_to_test:
+ print(f" ♿ Teste Accessibility: {page}")
+
+ # Alle Formulare auf der Seite finden und testen
+ page_results = await automator.test_all_forms_on_page(
+ page,
+ test_scenarios=["accessibility"]
+ )
+ all_results.extend(page_results)
+
+ # Report generieren
+ report_path = automator.generate_report("myp_accessibility_test")
+ print(f"📊 Accessibility Report: {report_path}")
+
+ return all_results
+
+
+async def run_comprehensive_myp_tests():
+ """
+ Führt alle Tests für das MYP-System durch.
+ """
+ print(f"""
+{'='*70}
+🧪 UMFASSENDE MYP FORMULAR-TESTS
+{'='*70}
+
+Testet alle wichtigen Formulare des MYP Systems:
+• Login & Authentifizierung
+• Drucker-Registrierung & -Verwaltung
+• Druckauftrags-Erstellung
+• Gast-Zugangs-System
+• Einstellungen & Konfiguration
+• Responsive Design
+• Accessibility
+
+{'='*70}
+""")
+
+ all_test_results = []
+ test_functions = [
+ ("Login-Formular", test_myp_login_form),
+ ("Drucker-Registrierung", test_myp_printer_registration),
+ ("Druckauftrags-Formular", test_myp_job_submission),
+ ("Gast-Zugang", test_myp_guest_access),
+ ("Einstellungen", test_myp_settings_forms),
+ ("Responsive Design", test_myp_responsive_design),
+ ("Accessibility", test_myp_accessibility)
+ ]
+
+ for test_name, test_function in test_functions:
+ try:
+ print(f"\n{'='*50}")
+ print(f"🔄 Starte {test_name}-Tests...")
+ print(f"{'='*50}")
+
+ results = await test_function()
+ all_test_results.extend(results)
+
+ # Kurze Statistik
+ passed = len([r for r in results if r.status == TestStatus.PASSED])
+ failed = len([r for r in results if r.status == TestStatus.FAILED])
+ total = len(results)
+
+ print(f"✅ {test_name} abgeschlossen: {passed}/{total} Tests bestanden")
+
+ except Exception as e:
+ print(f"❌ Fehler bei {test_name}: {str(e)}")
+
+ # Gesamt-Statistik
+ total_tests = len(all_test_results)
+ passed_tests = len([r for r in all_test_results if r.status == TestStatus.PASSED])
+ failed_tests = len([r for r in all_test_results if r.status == TestStatus.FAILED])
+ success_rate = (passed_tests / total_tests * 100) if total_tests > 0 else 0
+
+ print(f"""
+{'='*70}
+🎉 ALLE TESTS ABGESCHLOSSEN
+{'='*70}
+
+📊 GESAMT-STATISTIK:
+ • Gesamt Tests: {total_tests}
+ • ✅ Bestanden: {passed_tests} ({passed_tests/total_tests*100:.1f}%)
+ • ❌ Fehlgeschlagen: {failed_tests} ({failed_tests/total_tests*100:.1f}%)
+ • 🎯 Erfolgsrate: {success_rate:.1f}%
+
+💡 Reports wurden im Verzeichnis 'form_test_reports/' erstellt.
+
+{'='*70}
+""")
+
+ return all_test_results
+
+
+async def demo_single_form_test():
+ """
+ Einfaches Beispiel für einen einzelnen Formular-Test.
+ """
+ print("🎯 Demo: Einzelner Formular-Test")
+
+ async with HTMLFormTestAutomator(
+ base_url="http://localhost:5000",
+ browser="chromium",
+ headless=False # Browser-Fenster anzeigen für Demo
+ ) as automator:
+
+ # Login-Formular mit sichtbarem Browser testen
+ results = await automator.test_form(
+ url="/login",
+ form_selector="#login-form",
+ test_data={
+ "username": "demo_user",
+ "password": "demo_password"
+ },
+ test_scenarios=["valid", "invalid", "accessibility"]
+ )
+
+ # Report generieren
+ report_path = automator.generate_report("demo_single_test")
+ print(f"📊 Demo Report: {report_path}")
+
+ return results
+
+
+if __name__ == "__main__":
+ print("🚀 MYP Formular Test Automator - Beispiele")
+ print("Verfügbare Tests:")
+ print("1. Umfassende Tests (alle Formulare)")
+ print("2. Demo Einzeltest (mit sichtbarem Browser)")
+ print("3. Nur Login-Test")
+ print("4. Nur Responsive-Test")
+
+ choice = input("\nWähle einen Test (1-4): ").strip()
+
+ if choice == "1":
+ asyncio.run(run_comprehensive_myp_tests())
+ elif choice == "2":
+ asyncio.run(demo_single_form_test())
+ elif choice == "3":
+ asyncio.run(test_myp_login_form())
+ elif choice == "4":
+ asyncio.run(test_myp_responsive_design())
+ else:
+ print("Führe umfassende Tests aus...")
+ asyncio.run(run_comprehensive_myp_tests())
\ No newline at end of file