534 lines
28 KiB
Python
534 lines
28 KiB
Python
class BrowserService:
|
||
"""Сервис для автоматизации работы с браузером."""
|
||
# ...existing code...
|
||
|
||
def click_tab_and_wait(self, url, tab_text, wait_before_click=5, wait_after_click=5):
|
||
"""Открывает страницу, ждёт, нажимает вкладку по тексту, ждёт загрузку."""
|
||
try:
|
||
logging.info(f"🌐 Открываю страницу: {url}")
|
||
self.driver.get(url)
|
||
logging.info(f"⏳ Жду {wait_before_click} сек перед кликом...")
|
||
time.sleep(wait_before_click)
|
||
logging.info(f"🔍 Нажимаю вкладку: '{tab_text}'")
|
||
result = self.select_tab_by_multiple_methods(tab_text, timeout=10)
|
||
if result:
|
||
logging.info(f"✅ Вкладка '{tab_text}' нажата, жду {wait_after_click} сек...")
|
||
time.sleep(wait_after_click)
|
||
else:
|
||
logging.warning(f"❌ Не удалось нажать вкладку '{tab_text}'")
|
||
return result
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка click_tab_and_wait: {e}")
|
||
return False
|
||
"""
|
||
Модуль для автоматизации работы с браузером через Selenium
|
||
"""
|
||
import time
|
||
import logging
|
||
import os
|
||
import shutil
|
||
import sys
|
||
import platform
|
||
import subprocess
|
||
from pathlib import Path
|
||
from selenium import webdriver
|
||
from selenium.webdriver.common.by import By
|
||
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
||
from selenium.webdriver.firefox.service import Service as FirefoxService
|
||
from selenium.webdriver.support.ui import WebDriverWait
|
||
from selenium.webdriver.support import expected_conditions as EC
|
||
|
||
try:
|
||
from webdriver_manager.firefox import GeckoDriverManager
|
||
HAS_WEBDRIVER_MANAGER = True
|
||
except ImportError:
|
||
HAS_WEBDRIVER_MANAGER = False
|
||
|
||
|
||
class BrowserService:
|
||
"""Сервис для автоматизации работы с браузером."""
|
||
|
||
def __init__(self, download_dir, firefox_binary_path=None):
|
||
"""Инициализирует браузер с указанными параметрами."""
|
||
self.download_dir = download_dir
|
||
self.firefox_binary_path = firefox_binary_path or self._find_firefox_path()
|
||
self.driver = None
|
||
self.wait = None
|
||
self._setup_driver()
|
||
|
||
@staticmethod
|
||
def _find_firefox_path():
|
||
"""Автоматически находит путь к Firefox для текущей ОС."""
|
||
system = platform.system()
|
||
|
||
# 1. Проверка через shutil.which (Поиск в PATH) - работает и на Win и на Linux
|
||
path_from_which = shutil.which("firefox")
|
||
if path_from_which:
|
||
# На Windows shutil.which может вернуть .EXE, проверим существует ли
|
||
if os.path.exists(path_from_which):
|
||
logging.info(f"✅ Firefox найден в PATH: {path_from_which}")
|
||
return path_from_which
|
||
|
||
possible_paths = []
|
||
|
||
if system == "Windows":
|
||
# Windows пути - разные места установки Firefox
|
||
possible_paths = [
|
||
r"C:\Program Files\Mozilla Firefox\firefox.exe",
|
||
r"C:\Program Files (x86)\Mozilla Firefox\firefox.exe",
|
||
os.path.expandvars(r"%PROGRAMFILES%\Mozilla Firefox\firefox.exe"),
|
||
os.path.expandvars(r"%PROGRAMFILES(X86)%\Mozilla Firefox\firefox.exe"),
|
||
os.path.expandvars(r"%LOCALAPPDATA%\Mozilla Firefox\firefox.exe"),
|
||
os.path.expandvars(r"%APPDATA%\Mozilla Firefox\firefox.exe"),
|
||
]
|
||
|
||
# Пытаемся найти через реестр (Windows)
|
||
try:
|
||
import winreg
|
||
try:
|
||
reg_path = r"SOFTWARE\Mozilla\Mozilla Firefox"
|
||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path) as key:
|
||
install_dir = winreg.QueryValueEx(key, "InstallDir")[0]
|
||
possible_paths.insert(0, os.path.join(install_dir, "firefox.exe"))
|
||
except:
|
||
pass
|
||
except:
|
||
pass
|
||
|
||
elif system == "Darwin":
|
||
# macOS пути
|
||
possible_paths = [
|
||
"/Applications/Firefox.app/Contents/MacOS/firefox",
|
||
"/usr/local/bin/firefox",
|
||
]
|
||
|
||
else: # Linux
|
||
# Linux пути
|
||
possible_paths = [
|
||
"/usr/bin/firefox",
|
||
"/usr/bin/firefox-esr",
|
||
"/snap/bin/firefox",
|
||
"/usr/local/bin/firefox",
|
||
"/opt/firefox/firefox",
|
||
"/usr/lib/firefox/firefox",
|
||
]
|
||
|
||
# Ищем Firefox в возможных местах
|
||
for path in possible_paths:
|
||
if path and os.path.exists(path):
|
||
logging.info(f"✅ Firefox найден: {path}")
|
||
return path
|
||
|
||
# Последний шанс: subprocess (если shutil.which пропустил что-то специфичное)
|
||
try:
|
||
cmd = ["where", "firefox.exe"] if system == "Windows" else ["which", "firefox"]
|
||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
|
||
|
||
if result.returncode == 0 and result.stdout.strip():
|
||
firefox_path = result.stdout.strip().split('\n')[0]
|
||
if os.path.exists(firefox_path):
|
||
logging.info(f"✅ Firefox найден через команду search: {firefox_path}")
|
||
return firefox_path
|
||
except:
|
||
pass
|
||
|
||
logging.warning(f"⚠️ Firefox не найден в стандартных местах для {system}")
|
||
return None
|
||
|
||
def _setup_driver(self):
|
||
"""Настраивает и запускает веб-драйвер."""
|
||
try:
|
||
firefox_options = FirefoxOptions()
|
||
|
||
# ===== ОПЦИИ ЗАГРУЗКИ =====
|
||
firefox_options.set_preference("browser.download.folderList", 2)
|
||
firefox_options.set_preference("browser.download.dir", self.download_dir)
|
||
firefox_options.set_preference("browser.download.useDownloadDir", True)
|
||
firefox_options.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/octet-stream")
|
||
|
||
# Устанавливаем путь к Firefox только если он найден
|
||
if self.firefox_binary_path:
|
||
logging.info(f" ✅ Используется Firefox: {self.firefox_binary_path}")
|
||
firefox_options.binary_location = self.firefox_binary_path
|
||
else:
|
||
logging.info(" ⚠️ Firefox путь не определен, буду использовать системный поиск...")
|
||
|
||
logging.info("🚀 Запуск Firefox драйвера с ВИДИМЫМ окном...")
|
||
|
||
# Попытка 1: Использовать webdriver-manager (самый надежный способ)
|
||
if HAS_WEBDRIVER_MANAGER and not self.driver:
|
||
try:
|
||
logging.info(" Попытка 1: webdriver-manager...")
|
||
service = FirefoxService(GeckoDriverManager().install())
|
||
self.driver = webdriver.Firefox(service=service, options=firefox_options)
|
||
logging.info(" ✅ Успешно запущено через webdriver-manager")
|
||
except Exception as e:
|
||
logging.warning(f" webdriver-manager не сработал: {e}")
|
||
self.driver = None
|
||
|
||
# Попытка 2: Локальный драйвер в папке проекта
|
||
if not self.driver:
|
||
logging.info(" Попытка 2: Поиск локального geckodriver...")
|
||
system = platform.system()
|
||
driver_filename = "geckodriver.exe" if system == "Windows" else "geckodriver"
|
||
|
||
possible_driver_paths = [
|
||
os.path.join(os.getcwd(), driver_filename),
|
||
os.path.join(os.getcwd(), "drivers", driver_filename),
|
||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", driver_filename), # В корне проекта
|
||
]
|
||
|
||
for dp in possible_driver_paths:
|
||
if os.path.exists(dp):
|
||
try:
|
||
logging.info(f" ⚠️ Нашел локальный драйвер: {dp}")
|
||
service = FirefoxService(executable_path=dp)
|
||
self.driver = webdriver.Firefox(service=service, options=firefox_options)
|
||
logging.info(" ✅ Успешно запущено через локальный драйвер")
|
||
break
|
||
except Exception as e:
|
||
logging.warning(f" Ошибка локального драйвера {dp}: {e}")
|
||
|
||
# Попытка 3: Прямой запуск (Selenium сам найдет geckodriver в PATH)
|
||
if not self.driver:
|
||
try:
|
||
logging.info(" Попытка 3: Прямой запуск (Selenium ищет geckodriver в PATH)...")
|
||
self.driver = webdriver.Firefox(options=firefox_options)
|
||
logging.info(" ✅ Успешно запущено через системный PATH")
|
||
except Exception as e:
|
||
logging.warning(f" Прямой запуск не сработал: {e}")
|
||
self.driver = None
|
||
|
||
if not self.driver:
|
||
raise Exception("Не удалось инициализировать Firefox драйвер ни одним способом")
|
||
|
||
# Даем браузеру время открыться
|
||
time.sleep(2)
|
||
|
||
self.driver.set_page_load_timeout(300)
|
||
self.wait = WebDriverWait(self.driver, 10)
|
||
logging.info("✅ Браузер открыт и готов к работе.")
|
||
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка при настройке драйвера: {e}")
|
||
logging.error(f"\n Решения:")
|
||
logging.error(f" 1. Убедитесь, что Firefox установлен")
|
||
logging.error(f" 2. Скачайте geckodriver для вашей ОС и положите рядом с программой")
|
||
logging.error(f" 3. Ссылка на драйвер: https://github.com/mozilla/geckodriver/releases")
|
||
time.sleep(2)
|
||
|
||
self.driver.set_page_load_timeout(300)
|
||
self.wait = WebDriverWait(self.driver, 10)
|
||
logging.info("✅ Браузер открыт и готов к работе.")
|
||
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка при настройке драйвера: {e}")
|
||
logging.error(f"\n Решения:")
|
||
logging.error(f" 1. Убедитесь, что Firefox установлен")
|
||
logging.error(f" 2. На Windows: установите Firefox с https://mozilla.org/firefox")
|
||
|
||
raise
|
||
|
||
def open_page(self, url):
|
||
"""Открывает страницу по URL."""
|
||
try:
|
||
logging.info(f"🌐 Переход на страницу: {url}")
|
||
self.driver.get(url)
|
||
logging.info("✅ Страница загружена.")
|
||
time.sleep(2)
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка при открытии страницы: {e}")
|
||
raise
|
||
|
||
def wait_page_load(self, seconds=5):
|
||
"""Ждет загрузки страницы."""
|
||
logging.info(f"⏳ Ожидание {seconds} сек...")
|
||
time.sleep(seconds)
|
||
|
||
def click_button_by_text(self, button_text, exact_match=False, timeout=10):
|
||
"""
|
||
Ищет и нажимает кнопку (или любой элемент) по тексту, используя продвинутый метод поиска.
|
||
|
||
Args:
|
||
button_text (str): Текст кнопки для поиска
|
||
exact_match (bool): Игнорируется в новой реализации (используется умный поиск)
|
||
timeout (int): Время ожидания в секундах
|
||
|
||
Returns:
|
||
bool: True если кнопка найдена и нажата, False иначе
|
||
"""
|
||
logging.info(f"🔎 Ищу кнопку/элемент с текстом: '{button_text}' (Robust)")
|
||
return self.select_tab_by_multiple_methods(button_text, timeout)
|
||
|
||
def _find_button_by_text(self, button_text, exact_match=False, timeout=10):
|
||
# Этот метод больше не используется основным кодом, но оставлен для совместимости
|
||
# или если старый метод понадобится.
|
||
pass
|
||
|
||
|
||
def _text_matches(self, element_text, search_text, exact_match):
|
||
"""Проверяет совпадение текста."""
|
||
element_text_clean = element_text.lower().strip()
|
||
if exact_match:
|
||
return element_text_clean == search_text
|
||
else:
|
||
return search_text in element_text_clean
|
||
|
||
def _is_clickable(self, element):
|
||
"""Проверяет, можно ли кликнуть на элемент."""
|
||
try:
|
||
return element.is_displayed() and element.is_enabled()
|
||
except:
|
||
return False
|
||
|
||
def upload_file(self, file_path, selector='input[type="file"]', by=By.CSS_SELECTOR):
|
||
"""Загружает файл через input[type='file'] с максимальной надёжностью."""
|
||
try:
|
||
logging.info(f"📤 Загружаю файл: {Path(file_path).name}")
|
||
file_input = self.wait.until(EC.presence_of_element_located((by, selector)))
|
||
# Если input скрыт, делаем его видимым через JS
|
||
is_displayed = file_input.is_displayed()
|
||
if not is_displayed:
|
||
try:
|
||
self.driver.execute_script("arguments[0].style.display = 'block'; arguments[0].style.visibility = 'visible'; arguments[0].style.opacity = 1; arguments[0].style.height = 'auto'; arguments[0].style.width = 'auto';", file_input)
|
||
logging.info(" Сделал input[type='file'] видимым через JS")
|
||
except Exception as e:
|
||
logging.warning(f" Не удалось сделать input[type='file'] видимым: {e}")
|
||
file_input.send_keys(file_path)
|
||
logging.info(f"✅ Файл загружен: {Path(file_path).name}")
|
||
time.sleep(1)
|
||
return True
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка при загрузке файла {Path(file_path).name}: {e}")
|
||
return False
|
||
|
||
def close(self):
|
||
"""Закрывает браузер."""
|
||
if self.driver:
|
||
self.driver.quit()
|
||
logging.info("🛑 Браузер закрыт.")
|
||
|
||
def get_page_title(self):
|
||
"""Возвращает заголовок страницы."""
|
||
return self.driver.title
|
||
|
||
def open_new_tab(self, url):
|
||
"""Открывает новую вкладку с заданным URL."""
|
||
try:
|
||
logging.info(f"📑 Открываю новую вкладку: {url}")
|
||
# Открываем новую вкладку
|
||
self.driver.execute_script("window.open(arguments[0], '_blank');", url)
|
||
# Переключаемся на последнюю открытую вкладку
|
||
self.driver.switch_to.window(self.driver.window_handles[-1])
|
||
time.sleep(2)
|
||
logging.info("✅ Новая вкладка открыта и активна.")
|
||
return True
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка при открытии новой вкладки: {e}")
|
||
return False
|
||
|
||
def close_current_tab(self):
|
||
"""Закрывает текущую вкладку и переходит на предыдущую."""
|
||
try:
|
||
logging.info("📑 Метод close_current_tab вызван, но вкладки не будут закрываться.")
|
||
# Удаляем логику закрытия вкладок
|
||
return True
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка при попытке закрыть вкладку: {e}")
|
||
return False
|
||
|
||
def get_tabs_count(self):
|
||
"""Возвращает количество открытых вкладок."""
|
||
return len(self.driver.window_handles)
|
||
|
||
def select_tab_by_name(self, tab_name):
|
||
"""Выбирает вкладку на странице по её названию."""
|
||
try:
|
||
logging.info(f"🔍 Поиск вкладки с названием: {tab_name}")
|
||
# Ищем элемент вкладки по тексту
|
||
tab_element = self.wait.until(
|
||
EC.presence_of_element_located((By.XPATH, f"//a[text()='{tab_name}']"))
|
||
)
|
||
tab_element.click()
|
||
logging.info(f"✅ Вкладка '{tab_name}' выбрана.")
|
||
return True
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка при выборе вкладки '{tab_name}': {e}")
|
||
return False
|
||
|
||
def select_tab_by_text_or_name(self, target_text, timeout=30):
|
||
"""Ищет и выбирает вкладку по тексту или названию, делая повторные попытки.
|
||
|
||
Args:
|
||
target_text (str): Текст или название вкладки для поиска.
|
||
timeout (int): Общее время ожидания в секундах.
|
||
|
||
Returns:
|
||
bool: True, если вкладка найдена и выбрана, иначе False.
|
||
"""
|
||
end_time = time.time() + timeout
|
||
while time.time() < end_time:
|
||
try:
|
||
logging.info(f"🔍 Поиск вкладки с текстом или названием: {target_text}")
|
||
# Ищем элемент вкладки по тексту
|
||
tab_element = self.wait.until(
|
||
EC.presence_of_element_located((By.XPATH, f"//*[text()='{target_text}']"))
|
||
)
|
||
self.driver.execute_script("arguments[0].scrollIntoView(true);", tab_element)
|
||
time.sleep(0.5)
|
||
tab_element.click()
|
||
logging.info(f"✅ Вкладка с текстом '{target_text}' выбрана.")
|
||
return True
|
||
except Exception as e:
|
||
logging.warning(f"⚠️ Вкладка с текстом '{target_text}' не найдена, повторная попытка...")
|
||
time.sleep(1) # Ждем перед повторной попыткой
|
||
logging.error(f"❌ Не удалось найти вкладку с текстом '{target_text}' за {timeout} секунд.")
|
||
return False
|
||
|
||
def set_tabs(self, tabs):
|
||
"""Устанавливает массив с названиями вкладок."""
|
||
self.tabs = tabs
|
||
|
||
def select_tab_from_list(self, tab_index):
|
||
"""Выбирает вкладку из массива по индексу.
|
||
|
||
Args:
|
||
tab_index (int): Индекс вкладки в массиве.
|
||
|
||
Returns:
|
||
bool: True, если вкладка выбрана успешно, иначе False.
|
||
"""
|
||
try:
|
||
if not hasattr(self, 'tabs') or not self.tabs:
|
||
logging.error("❌ Массив вкладок не установлен или пуст.")
|
||
return False
|
||
|
||
if tab_index < 0 or tab_index >= len(self.tabs):
|
||
logging.error("❌ Индекс вкладки выходит за пределы массива.")
|
||
return False
|
||
|
||
tab_name = self.tabs[tab_index]
|
||
logging.info(f"🔍 Выбор вкладки с индексом {tab_index}: {tab_name}")
|
||
return self.select_tab_by_name(tab_name)
|
||
except Exception as e:
|
||
logging.error(f"❌ Ошибка при выборе вкладки по индексу {tab_index}: {e}")
|
||
return False
|
||
|
||
def select_tab_by_multiple_methods(self, tab_text, timeout=15):
|
||
"""Пытается выбрать вкладку или кнопку на странице перебором всех элементов.
|
||
|
||
Args:
|
||
tab_text (str): Текст вкладки или кнопки для поиска
|
||
timeout (int): Общее время ожидания в секундах
|
||
|
||
Returns:
|
||
bool: True, если элемент найден и выбран, иначе False
|
||
"""
|
||
from selenium.webdriver.common.action_chains import ActionChains
|
||
|
||
end_time = time.time() + timeout
|
||
attempt = 0
|
||
tab_text_normalized = tab_text.strip().lower()
|
||
logging.info(f"🔍 Начинаю поиск вкладки/кнопки: '{tab_text}' (v3)")
|
||
|
||
tag_selector = 'a, div, span, li, button, td, th, input, label, strong, b, em, i, p, section, header, footer, article, aside, nav, form'
|
||
|
||
while time.time() < end_time:
|
||
attempt += 1
|
||
logging.info(f" Попытка {attempt}: поиск элементов через JS...")
|
||
try:
|
||
script = f"""
|
||
var searchText = arguments[0];
|
||
var allElements = document.querySelectorAll('{tag_selector}');
|
||
var foundElements = [];
|
||
for (var i = 0; i < allElements.length; i++) {{
|
||
var el = allElements[i];
|
||
if (el.offsetParent === null) continue;
|
||
var elementText = (el.innerText || el.textContent || '').trim().toLowerCase();
|
||
var aria = (el.getAttribute('aria-label') || '').trim().toLowerCase();
|
||
var title = (el.getAttribute('title') || '').trim().toLowerCase();
|
||
if (
|
||
elementText == searchText ||
|
||
elementText.includes(searchText) ||
|
||
aria == searchText ||
|
||
aria.includes(searchText) ||
|
||
title == searchText ||
|
||
title.includes(searchText)
|
||
) {{
|
||
foundElements.push(el);
|
||
}}
|
||
var children = el.querySelectorAll('span, b, strong, em, i');
|
||
for (var j = 0; j < children.length; j++) {{
|
||
var childText = (children[j].innerText || children[j].textContent || '').trim().toLowerCase();
|
||
if (childText == searchText || childText.includes(searchText)) {{
|
||
foundElements.push(el);
|
||
break;
|
||
}}
|
||
}}
|
||
}}
|
||
return foundElements;
|
||
"""
|
||
candidates = self.driver.execute_script(script, tab_text_normalized)
|
||
if not candidates:
|
||
logging.info(" ⚠️ JS поиск ничего не дал, пробую XPath...")
|
||
try:
|
||
xpath = (
|
||
f"//*[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{tab_text_normalized}') "
|
||
f"or contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{tab_text_normalized}') "
|
||
f"or contains(translate(@title, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{tab_text_normalized}')]"
|
||
)
|
||
candidates = self.driver.find_elements(By.XPATH, xpath)
|
||
except Exception as e_xpath:
|
||
logging.warning(f" XPath fail: {e_xpath}")
|
||
|
||
logging.info(f" Найдено кандидатов: {len(candidates)}")
|
||
for i, element in enumerate(candidates):
|
||
try:
|
||
if not element.is_displayed():
|
||
continue
|
||
self.driver.execute_script("arguments[0].style.border='3px solid red';", element)
|
||
time.sleep(0.5)
|
||
try:
|
||
logging.info(f" 👉 Кандидат #{i+1} (<{element.tag_name}>): text='{element.text[:40]}', aria-label='{element.get_attribute('aria-label')}', title='{element.get_attribute('title')}'")
|
||
except Exception as elog:
|
||
logging.info(f" 👉 Кандидат #{i+1} (<unknown>): {elog}")
|
||
# Сохраняем текущее содержимое body
|
||
old_body = self.driver.execute_script("return document.body.innerHTML;")
|
||
# 1. ActionChains
|
||
try:
|
||
ActionChains(self.driver).move_to_element(element).click().perform()
|
||
logging.info(" ✅ ActionChains click OK")
|
||
self.driver.execute_script("arguments[0].style.border='';", element)
|
||
except Exception as e1:
|
||
logging.warning(f" ActionChains fail: {e1}")
|
||
# 2. Native Click
|
||
try:
|
||
element.click()
|
||
logging.info(" ✅ Element click OK")
|
||
self.driver.execute_script("arguments[0].style.border='';", element)
|
||
except Exception as e2:
|
||
logging.warning(f" Native click fail: {e2}")
|
||
# 3. JS Click
|
||
self.driver.execute_script("arguments[0].click();", element)
|
||
logging.info(" ✅ JS click OK")
|
||
self.driver.execute_script("arguments[0].style.border='';", element)
|
||
# Проверяем изменение содержимого body
|
||
try:
|
||
WebDriverWait(self.driver, 5).until(lambda d: d.execute_script("return document.body.innerHTML;") != old_body)
|
||
logging.info(" ✅ Содержимое страницы изменилось после клика")
|
||
return True
|
||
except Exception:
|
||
logging.warning(" ⚠️ Содержимое страницы не изменилось после клика")
|
||
continue
|
||
except Exception as e:
|
||
logging.error(f" Ошибка обработки элемента: {e}")
|
||
try:
|
||
self.driver.execute_script("arguments[0].style.border='';", element)
|
||
except:
|
||
pass
|
||
time.sleep(1)
|
||
except Exception as e:
|
||
logging.warning(f" ✗ Ошибка при попытке {attempt}: {e}")
|
||
time.sleep(1)
|
||
logging.error(f"❌ Не удалось найти и кликнуть на элемент '{tab_text}' за {timeout} секунд")
|
||
return False |