424 Zeilen
15 KiB
Python
424 Zeilen
15 KiB
Python
# social_networks/x/x_ui_helper.py
|
|
|
|
"""
|
|
X (Twitter) UI Helper - Hilfsklasse für UI-Interaktionen bei X
|
|
"""
|
|
|
|
import time
|
|
from typing import Optional, List, Dict, Any, Tuple
|
|
from playwright.sync_api import Page, ElementHandle
|
|
|
|
from .x_selectors import XSelectors
|
|
from utils.logger import setup_logger
|
|
|
|
# Konfiguriere Logger
|
|
logger = setup_logger("x_ui_helper")
|
|
|
|
class XUIHelper:
|
|
"""
|
|
Hilfsklasse für UI-Interaktionen mit X.
|
|
Bietet wiederverwendbare Methoden für häufige UI-Operationen.
|
|
"""
|
|
|
|
def __init__(self, automation):
|
|
"""
|
|
Initialisiert den X UI Helper.
|
|
|
|
Args:
|
|
automation: Referenz auf die Hauptautomatisierungsklasse
|
|
"""
|
|
self.automation = automation
|
|
self.selectors = XSelectors()
|
|
|
|
logger.debug("X UI Helper initialisiert")
|
|
|
|
def wait_for_element(self, selector: str, timeout: int = 10000,
|
|
state: str = "visible") -> Optional[ElementHandle]:
|
|
"""
|
|
Wartet auf ein Element und gibt es zurück.
|
|
|
|
Args:
|
|
selector: CSS-Selektor
|
|
timeout: Timeout in Millisekunden
|
|
state: Gewünschter Zustand ("visible", "attached", "detached", "hidden")
|
|
|
|
Returns:
|
|
Optional[ElementHandle]: Element oder None
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
element = page.wait_for_selector(selector, timeout=timeout, state=state)
|
|
return element
|
|
except Exception as e:
|
|
logger.debug(f"Element nicht gefunden: {selector} - {e}")
|
|
return None
|
|
|
|
def click_element_safely(self, selector: str, timeout: int = 10000,
|
|
retry_count: int = 3) -> bool:
|
|
"""
|
|
Klickt sicher auf ein Element mit Retry-Logik.
|
|
|
|
Args:
|
|
selector: CSS-Selektor
|
|
timeout: Timeout in Millisekunden
|
|
retry_count: Anzahl der Wiederholungsversuche
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, False bei Fehler
|
|
"""
|
|
page = self.automation.browser.page
|
|
|
|
for attempt in range(retry_count):
|
|
try:
|
|
# Warte auf Element
|
|
element = self.wait_for_element(selector, timeout)
|
|
if not element:
|
|
logger.warning(f"Element nicht gefunden beim {attempt + 1}. Versuch: {selector}")
|
|
continue
|
|
|
|
# Scrolle zum Element
|
|
element.scroll_into_view_if_needed()
|
|
|
|
# Warte kurz
|
|
self.automation.human_behavior.random_delay(0.3, 0.7)
|
|
|
|
# Klicke
|
|
element.click()
|
|
logger.info(f"Element erfolgreich geklickt: {selector}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Fehler beim Klicken (Versuch {attempt + 1}): {e}")
|
|
if attempt < retry_count - 1:
|
|
self.automation.human_behavior.random_delay(1, 2)
|
|
continue
|
|
|
|
logger.error(f"Konnte Element nicht klicken nach {retry_count} Versuchen: {selector}")
|
|
return False
|
|
|
|
def type_text_safely(self, selector: str, text: str, clear_first: bool = True,
|
|
timeout: int = 10000) -> bool:
|
|
"""
|
|
Gibt Text sicher in ein Eingabefeld ein.
|
|
|
|
Args:
|
|
selector: CSS-Selektor
|
|
text: Einzugebender Text
|
|
clear_first: Ob das Feld zuerst geleert werden soll
|
|
timeout: Timeout in Millisekunden
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, False bei Fehler
|
|
"""
|
|
try:
|
|
element = self.wait_for_element(selector, timeout)
|
|
if not element:
|
|
logger.error(f"Eingabefeld nicht gefunden: {selector}")
|
|
return False
|
|
|
|
# Fokussiere das Element
|
|
element.focus()
|
|
self.automation.human_behavior.random_delay(0.2, 0.5)
|
|
|
|
# Leere das Feld wenn gewünscht
|
|
if clear_first:
|
|
element.click(click_count=3) # Alles auswählen
|
|
self.automation.human_behavior.random_delay(0.1, 0.3)
|
|
element.press("Delete")
|
|
self.automation.human_behavior.random_delay(0.2, 0.5)
|
|
|
|
# Tippe den Text
|
|
self.automation.human_behavior.type_text(element, text)
|
|
|
|
logger.info(f"Text erfolgreich eingegeben in: {selector}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Texteingeben: {e}")
|
|
return False
|
|
|
|
def select_dropdown_option(self, selector: str, value: str, timeout: int = 10000) -> bool:
|
|
"""
|
|
Wählt eine Option aus einem Dropdown-Menü.
|
|
|
|
Args:
|
|
selector: CSS-Selektor des Select-Elements
|
|
value: Wert der zu wählenden Option
|
|
timeout: Timeout in Millisekunden
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, False bei Fehler
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
|
|
# Warte auf Select-Element
|
|
select = self.wait_for_element(selector, timeout)
|
|
if not select:
|
|
logger.error(f"Dropdown nicht gefunden: {selector}")
|
|
return False
|
|
|
|
# Wähle Option
|
|
page.select_option(selector, value)
|
|
|
|
logger.info(f"Option '{value}' ausgewählt in: {selector}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Auswählen der Dropdown-Option: {e}")
|
|
return False
|
|
|
|
def handle_modal(self, action: str = "accept", timeout: int = 5000) -> bool:
|
|
"""
|
|
Behandelt modale Dialoge.
|
|
|
|
Args:
|
|
action: "accept", "dismiss" oder "close"
|
|
timeout: Timeout in Millisekunden
|
|
|
|
Returns:
|
|
bool: True wenn Modal behandelt wurde, False sonst
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
|
|
# Prüfe ob Modal vorhanden
|
|
modal = self.wait_for_element(self.selectors.MODALS["modal_container"], timeout)
|
|
if not modal:
|
|
logger.debug("Kein Modal gefunden")
|
|
return False
|
|
|
|
if action == "accept":
|
|
# Suche Bestätigen-Button
|
|
if self.click_element_safely(self.selectors.MODALS["confirm_button"], timeout=3000):
|
|
logger.info("Modal bestätigt")
|
|
return True
|
|
|
|
elif action == "dismiss":
|
|
# Suche Abbrechen-Button
|
|
if self.click_element_safely(self.selectors.MODALS["cancel_button"], timeout=3000):
|
|
logger.info("Modal abgebrochen")
|
|
return True
|
|
|
|
elif action == "close":
|
|
# Suche Schließen-Button
|
|
close_selectors = [
|
|
self.selectors.MODALS["modal_close_button"],
|
|
self.selectors.MODALS["modal_close_button_en"]
|
|
]
|
|
for selector in close_selectors:
|
|
if self.click_element_safely(selector, timeout=3000):
|
|
logger.info("Modal geschlossen")
|
|
return True
|
|
|
|
logger.warning(f"Konnte Modal nicht mit Aktion '{action}' behandeln")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei Modal-Behandlung: {e}")
|
|
return False
|
|
|
|
def check_for_errors(self, timeout: int = 2000) -> Optional[str]:
|
|
"""
|
|
Prüft auf Fehlermeldungen auf der Seite.
|
|
|
|
Args:
|
|
timeout: Timeout in Millisekunden
|
|
|
|
Returns:
|
|
Optional[str]: Fehlermeldung wenn gefunden, sonst None
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
|
|
# Prüfe alle Fehler-Selektoren
|
|
for category, selector in [
|
|
("error_message", self.selectors.ERRORS["error_message"]),
|
|
("error_alert", self.selectors.ERRORS["error_alert"]),
|
|
("rate_limit", self.selectors.ERRORS["rate_limit_message"]),
|
|
("suspended", self.selectors.ERRORS["suspended_message"]),
|
|
("email_taken", self.selectors.ERRORS["email_taken"]),
|
|
("invalid_credentials", self.selectors.ERRORS["invalid_credentials"])
|
|
]:
|
|
error_element = self.wait_for_element(selector, timeout=timeout)
|
|
if error_element:
|
|
error_text = error_element.text_content()
|
|
logger.warning(f"Fehler gefunden ({category}): {error_text}")
|
|
return error_text
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Fehler bei Fehlerprüfung: {e}")
|
|
return None
|
|
|
|
def wait_for_navigation(self, timeout: int = 30000) -> bool:
|
|
"""
|
|
Wartet auf Navigation/Seitenwechsel.
|
|
|
|
Args:
|
|
timeout: Timeout in Millisekunden
|
|
|
|
Returns:
|
|
bool: True wenn Navigation erfolgt ist
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
|
|
with page.expect_navigation(timeout=timeout):
|
|
pass
|
|
|
|
logger.info("Navigation abgeschlossen")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Keine Navigation erkannt: {e}")
|
|
return False
|
|
|
|
def scroll_to_bottom(self, smooth: bool = True, pause_time: float = 1.0):
|
|
"""
|
|
Scrollt zum Ende der Seite.
|
|
|
|
Args:
|
|
smooth: Ob sanft gescrollt werden soll
|
|
pause_time: Pausenzeit nach dem Scrollen
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
|
|
if smooth:
|
|
# Sanftes Scrollen in Schritten
|
|
page.evaluate("""
|
|
async () => {
|
|
const distance = 100;
|
|
const delay = 50;
|
|
const timer = setInterval(() => {
|
|
window.scrollBy(0, distance);
|
|
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
|
clearInterval(timer);
|
|
}
|
|
}, delay);
|
|
}
|
|
""")
|
|
else:
|
|
# Direktes Scrollen
|
|
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
|
|
|
time.sleep(pause_time)
|
|
logger.debug("Zum Seitenende gescrollt")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Scrollen: {e}")
|
|
|
|
def take_element_screenshot(self, selector: str, filename: str,
|
|
timeout: int = 10000) -> bool:
|
|
"""
|
|
Macht einen Screenshot von einem spezifischen Element.
|
|
|
|
Args:
|
|
selector: CSS-Selektor
|
|
filename: Dateiname für den Screenshot
|
|
timeout: Timeout in Millisekunden
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, False bei Fehler
|
|
"""
|
|
try:
|
|
element = self.wait_for_element(selector, timeout)
|
|
if not element:
|
|
logger.error(f"Element für Screenshot nicht gefunden: {selector}")
|
|
return False
|
|
|
|
# Screenshot vom Element
|
|
element.screenshot(path=f"{self.automation.screenshots_dir}/{filename}")
|
|
logger.info(f"Element-Screenshot gespeichert: {filename}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Element-Screenshot: {e}")
|
|
return False
|
|
|
|
def get_element_text(self, selector: str, timeout: int = 10000) -> Optional[str]:
|
|
"""
|
|
Holt den Text eines Elements.
|
|
|
|
Args:
|
|
selector: CSS-Selektor
|
|
timeout: Timeout in Millisekunden
|
|
|
|
Returns:
|
|
Optional[str]: Text des Elements oder None
|
|
"""
|
|
try:
|
|
element = self.wait_for_element(selector, timeout)
|
|
if element:
|
|
return element.text_content()
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen des Element-Texts: {e}")
|
|
return None
|
|
|
|
def is_element_visible(self, selector: str, timeout: int = 1000) -> bool:
|
|
"""
|
|
Prüft ob ein Element sichtbar ist.
|
|
|
|
Args:
|
|
selector: CSS-Selektor
|
|
timeout: Timeout in Millisekunden
|
|
|
|
Returns:
|
|
bool: True wenn sichtbar, False sonst
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
# is_visible hat kein timeout parameter in Playwright
|
|
# Verwende wait_for_selector für timeout-Funktionalität
|
|
try:
|
|
element = page.wait_for_selector(selector, timeout=timeout, state="visible")
|
|
return element is not None
|
|
except:
|
|
return False
|
|
except Exception as e:
|
|
logger.debug(f"Element nicht sichtbar: {selector} - {e}")
|
|
return False
|
|
|
|
def wait_for_any_selector(self, selectors: List[str], timeout: int = 10000) -> Optional[Tuple[str, ElementHandle]]:
|
|
"""
|
|
Wartet auf eines von mehreren Elementen.
|
|
|
|
Args:
|
|
selectors: Liste von CSS-Selektoren
|
|
timeout: Timeout in Millisekunden
|
|
|
|
Returns:
|
|
Optional[Tuple[str, ElementHandle]]: Tuple aus Selektor und Element oder None
|
|
"""
|
|
try:
|
|
page = self.automation.browser.page
|
|
|
|
# Erstelle Promise für jeden Selektor
|
|
promises = []
|
|
for selector in selectors:
|
|
promises.append(page.wait_for_selector(selector, timeout=timeout))
|
|
|
|
# Warte auf das erste Element
|
|
element = page.evaluate(f"""
|
|
() => {{
|
|
const selectors = {selectors};
|
|
for (const selector of selectors) {{
|
|
const element = document.querySelector(selector);
|
|
if (element) return {{selector, found: true}};
|
|
}}
|
|
return {{found: false}};
|
|
}}
|
|
""")
|
|
|
|
if element["found"]:
|
|
actual_element = page.query_selector(element["selector"])
|
|
return (element["selector"], actual_element)
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Keines der Elemente gefunden: {e}")
|
|
return None |