From 9a03e522090593e3f66174697087995378a1eb88 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Wed, 18 Jun 2025 07:24:27 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Added=20new=20form=20testing=20t?= =?UTF-8?q?ools=20and=20examples=20to=20the=20backend=20=F0=9F=93=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/README_FORM_TESTER.md | 393 ++++ backend/form_test_automator.py | 2465 ++++++++++++++++++++++++++ backend/form_tester_setup.sh | 223 +++ backend/requirements_form_tester.txt | 37 + backend/test_forms_example.py | 555 ++++++ 5 files changed, 3673 insertions(+) create mode 100644 backend/README_FORM_TESTER.md create mode 100644 backend/form_test_automator.py create mode 100644 backend/form_tester_setup.sh create mode 100644 backend/requirements_form_tester.txt create mode 100644 backend/test_forms_example.py 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 + + + +
+
+

🧪 Formular Test Report

+

{report_name}

+

Generiert am {datetime.now().strftime('%d.%m.%Y um %H:%M:%S')}

+
+ +
+
+
{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""" +
+
+

📝 Formular: {form_selector}

+

{len(results)} Tests durchgeführt

+
+
+""" + + for result in results: + status_class = result.status.value.split()[0].lower().replace('✅', 'passed').replace('❌', 'failed').replace('⚠️', 'warning') + + html += f""" +
+
{result.status.value}
+
+ {result.test_type.replace('_', ' ').title()}
+ {result.message} + {f' ({result.execution_time:.2f}s)' if result.execution_time > 0 else ''} +
+
+""" + + # Details hinzufügen wenn vorhanden + if result.details: + html += f""" +
+ Details:
+
{json.dumps(result.details, indent=2, ensure_ascii=False)}
+
+""" + + # Screenshot hinzufügen wenn vorhanden + if result.screenshot_path and Path(result.screenshot_path).exists(): + rel_path = Path(result.screenshot_path).relative_to(self.output_dir) + html += f""" +
+ Screenshot für {result.test_type} +
+""" + + html += """ +
+
+""" + + 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