#!/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""" {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())