Files
Projektarbeit-MYP/backend/form_test_automator.py

2493 lines
99 KiB
Python

#!/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
"<script>alert('xss')</script>", # 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"""
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{report_name} - Formular Test Report</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}}
.stats {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 30px;
background: #f8f9fa;
}}
.stat-card {{
background: white;
padding: 20px;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.stat-number {{
font-size: 2.5em;
font-weight: bold;
margin-bottom: 10px;
}}
.passed {{ color: #28a745; }}
.failed {{ color: #dc3545; }}
.warning {{ color: #ffc107; }}
.content {{
padding: 30px;
}}
.form-section {{
margin-bottom: 40px;
border: 1px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
}}
.form-header {{
background: #e9ecef;
padding: 15px 20px;
border-bottom: 1px solid #dee2e6;
}}
.test-results {{
padding: 20px;
}}
.test-item {{
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f1f3f4;
}}
.test-item:last-child {{
border-bottom: none;
}}
.test-status {{
width: 150px;
font-weight: bold;
}}
.test-message {{
flex: 1;
}}
.test-details {{
background: #f8f9fa;
padding: 10px;
margin-top: 10px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}}
.screenshot {{
max-width: 100%;
border-radius: 4px;
margin: 10px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}}
.footer {{
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #6c757d;
border-top: 1px solid #dee2e6;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 Formular Test Report</h1>
<h2>{report_name}</h2>
<p>Generiert am {datetime.now().strftime('%d.%m.%Y um %H:%M:%S')}</p>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number">{total_tests}</div>
<div>Gesamt Tests</div>
</div>
<div class="stat-card">
<div class="stat-number passed">{passed_tests}</div>
<div>Bestanden</div>
</div>
<div class="stat-card">
<div class="stat-number failed">{failed_tests}</div>
<div>Fehlgeschlagen</div>
</div>
<div class="stat-card">
<div class="stat-number warning">{warning_tests}</div>
<div>Warnungen</div>
</div>
<div class="stat-card">
<div class="stat-number" style="color: {'#28a745' if success_rate >= 80 else '#ffc107' if success_rate >= 60 else '#dc3545'}">{success_rate:.1f}%</div>
<div>Erfolgsrate</div>
</div>
</div>
<div class="content">
"""
# Formular-spezifische Ergebnisse
for form_selector, results in forms_results.items():
html += f"""
<div class="form-section">
<div class="form-header">
<h3>📝 Formular: {form_selector}</h3>
<p>{len(results)} Tests durchgeführt</p>
</div>
<div class="test-results">
"""
for result in results:
status_class = result.status.value.split()[0].lower().replace('', 'passed').replace('', 'failed').replace('⚠️', 'warning')
html += f"""
<div class="test-item">
<div class="test-status {status_class}">{result.status.value}</div>
<div class="test-message">
<strong>{result.test_type.replace('_', ' ').title()}</strong><br>
{result.message}
{f'<small> ({result.execution_time:.2f}s)</small>' if result.execution_time > 0 else ''}
</div>
</div>
"""
# Details hinzufügen wenn vorhanden
if result.details:
html += f"""
<div class="test-details">
<strong>Details:</strong><br>
<pre>{json.dumps(result.details, indent=2, ensure_ascii=False)}</pre>
</div>
"""
# 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"""
<div>
<img src="{rel_path}" alt="Screenshot für {result.test_type}" class="screenshot">
</div>
"""
html += """
</div>
</div>
"""
html += f"""
</div>
<div class="footer">
<p>🚀 Generiert mit Flask HTML-Formular Test Automator</p>
<p>Mercedes-Benz Projektarbeit MYP - Till Tomczak</p>
</div>
</div>
</body>
</html>
"""
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())