Initial commit
Dieser Commit ist enthalten in:
424
social_networks/x/x_ui_helper.py
Normale Datei
424
social_networks/x/x_ui_helper.py
Normale Datei
@ -0,0 +1,424 @@
|
||||
# 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
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren