📝 Formular: {form_selector}
{len(results)} Tests durchgeführt
{json.dumps(result.details, indent=2, ensure_ascii=False)}
#!/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") # Fallback-Klassen für Type Hints class Page: pass class Browser: pass class BrowserContext: pass # 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") # Fallback-Klasse class Faker: def __init__(self, locale='de_DE'): pass def email(self): return "test@example.com" def name(self): return "Test User" def first_name(self): return "Test" def last_name(self): return "User" def company(self): return "Test Company" def city(self): return "Test City" def street_address(self): return "Test Street 123" def phone_number(self): return "+49 123 4567890" def url(self): return "https://example.com" def date(self): return "2024-06-18" def text(self, max_nb_chars=200): return "Test text" # 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") # Fallback-Klasse class BeautifulSoup: def __init__(self, *args, **kwargs): pass # 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 # Fallback-Klassen class Console: def print(self, *args, **kwargs): print(*args) class Table: def __init__(self, *args, **kwargs): pass def add_column(self, *args, **kwargs): pass def add_row(self, *args, **kwargs): pass 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"""
Generiert am {datetime.now().strftime('%d.%m.%Y um %H:%M:%S')}
{len(results)} Tests durchgeführt
{json.dumps(result.details, indent=2, ensure_ascii=False)}