645 lines
25 KiB
Python
645 lines
25 KiB
Python
"""Модуль для работы с полями формы."""
|
||
|
||
import re
|
||
from typing import Dict, Optional, List, Callable
|
||
from playwright.sync_api import Page, Locator
|
||
from tools.logger import get_logger
|
||
|
||
logger = get_logger("FORM_FIELD_COMPONENT")
|
||
|
||
|
||
class FormFieldComponent:
|
||
"""Компонент для работы с полями формы."""
|
||
|
||
def __init__(self, page: Page):
|
||
"""
|
||
Инициализирует компонент для работы с полями формы.
|
||
|
||
Args:
|
||
page (Page): Экземпляр страницы Playwright
|
||
"""
|
||
self.page = page
|
||
self._form_fields = None
|
||
|
||
# Колбэк для загрузки полей
|
||
self._load_form_fields_callback = None
|
||
|
||
def set_form_fields(self, form_fields: Dict[str, Locator]) -> None:
|
||
"""
|
||
Устанавливает поля формы для работы.
|
||
|
||
Args:
|
||
form_fields: Словарь с полями формы {название: локатор}
|
||
"""
|
||
self._form_fields = form_fields
|
||
logger.debug(f"Set {len(form_fields)} form fields")
|
||
|
||
def set_load_form_fields_callback(self, callback: Callable[[], Dict[str, Locator]]) -> None:
|
||
"""
|
||
Устанавливает колбэк для ленивой загрузки полей формы.
|
||
|
||
Args:
|
||
callback: Функция, которая возвращает словарь полей формы
|
||
"""
|
||
self._load_form_fields_callback = callback
|
||
|
||
def get_available_fields(self) -> list:
|
||
"""
|
||
Получает список доступных полей.
|
||
Если поля не загружены и есть колбэк для загрузки - загружает их.
|
||
|
||
Returns:
|
||
list: Список названий полей
|
||
"""
|
||
if not self._form_fields:
|
||
if self._load_form_fields_callback:
|
||
logger.debug("Lazy loading form fields via callback...")
|
||
form_fields = self._load_form_fields_callback()
|
||
self.set_form_fields(form_fields)
|
||
else:
|
||
logger.warning("No form fields set and no load callback available")
|
||
return []
|
||
|
||
return list(self._form_fields.keys())
|
||
|
||
def get_combobox_options(self, field_name: str) -> List[str]:
|
||
"""
|
||
Получает список доступных опций из combobox поля.
|
||
|
||
Args:
|
||
field_name: Название combobox поля
|
||
|
||
Returns:
|
||
list[str]: Список доступных опций
|
||
"""
|
||
if not self._form_fields:
|
||
logger.warning("No form fields set")
|
||
return []
|
||
|
||
if field_name not in self._form_fields:
|
||
logger.debug(f"Combobox field '{field_name}' not found")
|
||
return []
|
||
|
||
field_container = self._form_fields[field_name]
|
||
|
||
try:
|
||
# Открываем combobox
|
||
if not self._open_combobox(field_container):
|
||
return []
|
||
|
||
# Получаем опции из открытого меню
|
||
options = self._get_dropdown_options()
|
||
|
||
# Закрываем combobox
|
||
self.page.keyboard.press("Escape")
|
||
self.page.wait_for_timeout(500)
|
||
|
||
logger.debug(f"Found {len(options)} options for '{field_name}': {options}")
|
||
return options
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting combobox options for '{field_name}': {e}")
|
||
self.page.keyboard.press("Escape")
|
||
return []
|
||
|
||
def get_selected_combobox_value(self, field_name: str) -> str:
|
||
"""
|
||
Получает выбранное значение из combobox.
|
||
|
||
Args:
|
||
field_name: Название combobox поля
|
||
|
||
Returns:
|
||
str: Выбранное значение или пустая строка если ничего не выбрано
|
||
"""
|
||
if not self._form_fields:
|
||
logger.warning("No form fields set")
|
||
return ""
|
||
|
||
if field_name not in self._form_fields:
|
||
logger.debug(f"Combobox field '{field_name}' not found")
|
||
return ""
|
||
|
||
field_container = self._form_fields[field_name]
|
||
|
||
try:
|
||
# Ищем span элементы с текстом
|
||
span_locator = field_container.locator("span")
|
||
if span_locator.count() == 0:
|
||
# Пробуем найти в input
|
||
input_locator = field_container.locator("input")
|
||
if input_locator.count() > 0:
|
||
return input_locator.first.input_value().strip()
|
||
return ""
|
||
|
||
# Ищем непустой текст в span
|
||
for i in range(span_locator.count()):
|
||
span_text = span_locator.nth(i).text_content().strip()
|
||
if span_text:
|
||
# Пропускаем заголовочные или системные тексты
|
||
if any(skip_text in span_text.lower() for skip_text in ['выберите', 'select', 'не выбрано']):
|
||
continue
|
||
logger.debug(f"Selected value for '{field_name}': '{span_text}'")
|
||
return span_text
|
||
|
||
return ""
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting selected value for '{field_name}': {e}")
|
||
return ""
|
||
|
||
def get_field_value(self, field_name: str) -> str:
|
||
"""
|
||
Получает значение поля (универсальный метод для разных типов полей).
|
||
|
||
Args:
|
||
field_name: Название поля
|
||
|
||
Returns:
|
||
str: Значение поля или пустая строка если поле не найдено
|
||
"""
|
||
if not self._form_fields or field_name not in self._form_fields:
|
||
return ""
|
||
|
||
field_container = self._form_fields[field_name]
|
||
|
||
try:
|
||
# Пробуем получить значение input
|
||
input_field = field_container.locator("input, textarea").first
|
||
if input_field.count() > 0:
|
||
return input_field.input_value().strip()
|
||
|
||
# Для combobox получаем выбранное значение
|
||
combobox_value = self.get_selected_combobox_value(field_name)
|
||
if combobox_value:
|
||
return combobox_value
|
||
|
||
# Для чекбокса получаем состояние
|
||
checkbox_state = self.is_checkbox_checked(field_name)
|
||
if checkbox_state is not None:
|
||
return "checked" if checkbox_state else "unchecked"
|
||
|
||
return ""
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting value for field '{field_name}': {e}")
|
||
return ""
|
||
|
||
def set_checkbox_field(self, field_name: str, checked: bool, checkbox_container_locator: str = None) -> bool:
|
||
"""
|
||
Устанавливает состояние чекбокса по полному совпадению названия или локатору контейнера.
|
||
|
||
Args:
|
||
field_name: Название поля (для поиска по метке)
|
||
checked: True - включить, False - выключить
|
||
checkbox_container_locator: Опциональный локатор контейнера чекбокса для прямого поиска
|
||
|
||
Returns:
|
||
bool: True если успешно, False если нет
|
||
"""
|
||
logger.debug(f"Setting checkbox: field_name='{field_name}', checked={checked}, container_locator={checkbox_container_locator}")
|
||
|
||
try:
|
||
# Находим чекбокс
|
||
checkbox = self._find_checkbox(field_name, checkbox_container_locator)
|
||
if checkbox is None:
|
||
logger.warning(f"Checkbox '{field_name}' not found")
|
||
return False
|
||
|
||
# Получаем текущее состояние
|
||
current_state = self._get_checkbox_state(checkbox)
|
||
|
||
# Если уже в нужном состоянии
|
||
if current_state is not None and current_state == checked:
|
||
logger.debug(f"Checkbox '{field_name}' already in desired state ({checked})")
|
||
return True
|
||
|
||
# Устанавливаем нужное состояние
|
||
if checked:
|
||
checkbox.check(force=True)
|
||
else:
|
||
checkbox.uncheck(force=True)
|
||
|
||
self.page.wait_for_timeout(500)
|
||
|
||
# Проверяем результат
|
||
new_state = self._get_checkbox_state(checkbox)
|
||
if new_state is not None and new_state == checked:
|
||
logger.info(f"✓ Checkbox '{field_name}' set to: {checked}")
|
||
return True
|
||
else:
|
||
logger.warning(f"Checkbox '{field_name}' setting failed. Expected: {checked}, got: {new_state}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error setting checkbox '{field_name}': {e}")
|
||
return False
|
||
|
||
def is_checkbox_checked(self, field_name: str, checkbox_container_locator: str = None) -> Optional[bool]:
|
||
"""
|
||
Проверяет состояние чекбокса.
|
||
|
||
Args:
|
||
field_name: Название чекбокс поля
|
||
checkbox_container_locator: Опциональный локатор контейнера чекбокса для прямого поиска
|
||
|
||
Returns:
|
||
bool: True если включен, False если выключен, None если поле не найдено или произошла ошибка
|
||
"""
|
||
logger.debug(f"Checking checkbox state: field_name='{field_name}', container_locator={checkbox_container_locator}")
|
||
|
||
try:
|
||
checkbox = self._find_checkbox(field_name, checkbox_container_locator)
|
||
if checkbox is None:
|
||
return None
|
||
|
||
return self._get_checkbox_state(checkbox)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error checking checkbox state for '{field_name}': {e}")
|
||
return None
|
||
|
||
def _find_checkbox(self, field_name: str, checkbox_container_locator: str = None) -> Optional[Locator]:
|
||
"""
|
||
Находит чекбокс по названию поля или локатору контейнера.
|
||
|
||
Returns:
|
||
Locator: Локатор чекбокса или None если не найден
|
||
"""
|
||
# 1. Поиск по локатору контейнера (если указан)
|
||
if checkbox_container_locator:
|
||
try:
|
||
# Ищем контейнер чекбокса
|
||
container = self.page.locator(checkbox_container_locator).first
|
||
|
||
if container.count() > 0:
|
||
# Ищем input чекбокса внутри контейнера
|
||
checkbox = container.locator("input[type='checkbox']").first
|
||
if checkbox.count() > 0:
|
||
logger.debug(f"Found checkbox in container: {checkbox_container_locator}")
|
||
return checkbox
|
||
|
||
# Ищем элемент с role='checkbox' внутри контейнера
|
||
checkbox = container.locator("[role='checkbox']").first
|
||
if checkbox.count() > 0:
|
||
logger.debug(f"Found checkbox by role in container: {checkbox_container_locator}")
|
||
return checkbox
|
||
|
||
logger.debug(f"Checkbox container not found: {checkbox_container_locator}")
|
||
except Exception as e:
|
||
logger.error(f"Error finding checkbox by container locator '{checkbox_container_locator}': {e}")
|
||
|
||
# 2. Поиск по названию поля (если указаны form_fields)
|
||
if field_name and self._form_fields and field_name in self._form_fields:
|
||
try:
|
||
field_container = self._form_fields[field_name]
|
||
field_container.scroll_into_view_if_needed()
|
||
self.page.wait_for_timeout(300)
|
||
|
||
checkbox = field_container.locator("input[type='checkbox'], [role='checkbox']").first
|
||
if checkbox.count() > 0:
|
||
logger.debug(f"Found checkbox by field name: {field_name}")
|
||
return checkbox
|
||
except Exception as e:
|
||
logger.error(f"Error finding checkbox by field name '{field_name}': {e}")
|
||
|
||
# 3. Поиск по тексту метки (fallback)
|
||
if field_name:
|
||
try:
|
||
# Ищем label с текстом, затем связанный чекбокс
|
||
label = self.page.locator(f"label:has-text('{field_name}')").first
|
||
if label.count() > 0:
|
||
# Ищем по атрибуту for
|
||
label_for = label.get_attribute("for")
|
||
if label_for:
|
||
checkbox = self.page.locator(f"#{label_for}").first
|
||
if checkbox.count() > 0:
|
||
return checkbox
|
||
|
||
# Ищем чекбокс рядом с label
|
||
checkbox = label.locator("..").locator("input[type='checkbox'], [role='checkbox']").first
|
||
if checkbox.count() > 0:
|
||
return checkbox
|
||
except Exception as e:
|
||
logger.error(f"Error finding checkbox by label text '{field_name}': {e}")
|
||
|
||
logger.warning(f"Checkbox '{field_name}' not found")
|
||
return None
|
||
|
||
def _get_checkbox_state(self, checkbox: Locator) -> Optional[bool]:
|
||
"""
|
||
Получает текущее состояние чекбокса.
|
||
Используется внутри is_checkbox_checked() и set_checkbox_field().
|
||
"""
|
||
try:
|
||
# 1. aria-checked атрибут
|
||
aria_checked = checkbox.get_attribute("aria-checked")
|
||
if aria_checked == "true":
|
||
return True
|
||
elif aria_checked == "false":
|
||
return False
|
||
|
||
# 2. checked атрибут
|
||
checked_attr = checkbox.get_attribute("checked")
|
||
if checked_attr is not None:
|
||
return True
|
||
|
||
# 3. метод is_checked()
|
||
try:
|
||
return checkbox.is_checked()
|
||
except:
|
||
pass
|
||
|
||
# 4. По классу иконки (для Vuetify)
|
||
icon = checkbox.locator(".v-icon, i").first
|
||
if icon.count() > 0:
|
||
icon_class = icon.get_attribute("class") or ""
|
||
if any(marked in icon_class for marked in ["mdi-checkbox-marked", "mdi-check", "check_box"]):
|
||
return True
|
||
elif any(unmarked in icon_class for unmarked in ["mdi-checkbox-blank-outline", "mdi-checkbox-blank", "check_box_outline_blank"]):
|
||
return False
|
||
|
||
logger.debug("Could not determine checkbox state")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.debug(f"Error getting checkbox state: {e}")
|
||
return None
|
||
|
||
def _open_combobox(self, field_container: Locator) -> bool:
|
||
"""
|
||
Открывает выпадающий список combobox.
|
||
|
||
Args:
|
||
field_container: Локатор контейнера поля
|
||
|
||
Returns:
|
||
bool: True если успешно открыт, False если нет
|
||
"""
|
||
try:
|
||
field_container.scroll_into_view_if_needed()
|
||
self.page.wait_for_timeout(300)
|
||
|
||
# Ищем кнопку открытия dropdown
|
||
dropdown_button = field_container.locator(".v-input__append-inner, [role='button']").first
|
||
|
||
if dropdown_button.count() == 0:
|
||
# Может быть поле уже открыто или нужно кликнуть на input
|
||
input_field = field_container.locator("input").first
|
||
input_field.click()
|
||
self.page.wait_for_timeout(1000)
|
||
else:
|
||
dropdown_button.click()
|
||
self.page.wait_for_timeout(1000)
|
||
|
||
# Проверяем что меню открылось
|
||
return self._is_dropdown_opened()
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error opening combobox: {e}")
|
||
return False
|
||
|
||
def _is_dropdown_opened(self) -> bool:
|
||
"""
|
||
Проверяет, открыт ли выпадающий список.
|
||
|
||
Returns:
|
||
bool: True если открыт, False если нет
|
||
"""
|
||
menu_selectors = [
|
||
".v-menu__content.menuable__content__active",
|
||
".v-select__menu",
|
||
".v-autocomplete__content",
|
||
".v-menu__content"
|
||
]
|
||
|
||
for selector in menu_selectors:
|
||
menu = self.page.locator(selector).first
|
||
if menu.count() > 0 and menu.is_visible():
|
||
return True
|
||
|
||
return False
|
||
|
||
def _get_dropdown_options(self) -> List[str]:
|
||
"""
|
||
Получает опции из открытого выпадающего списка.
|
||
|
||
Returns:
|
||
list[str]: Список опций
|
||
"""
|
||
menu_selectors = [
|
||
".v-menu__content.menuable__content__active",
|
||
".v-select__menu",
|
||
".v-autocomplete__content",
|
||
".v-menu__content"
|
||
]
|
||
|
||
for selector in menu_selectors:
|
||
menu = self.page.locator(selector).first
|
||
if menu.count() > 0 and menu.is_visible():
|
||
# Получаем все элементы списка
|
||
items = menu.locator("div[role='listitem'], .v-list-item")
|
||
if items.count() == 0:
|
||
return []
|
||
|
||
options = []
|
||
for i in range(items.count()):
|
||
text = items.nth(i).text_content().strip()
|
||
if text:
|
||
options.append(text)
|
||
|
||
return options
|
||
|
||
return []
|
||
|
||
def check_combobox_has_option(self, field_name: str, option_text: str) -> bool:
|
||
"""
|
||
Проверяет наличие опции в combobox.
|
||
|
||
Args:
|
||
field_name: Название combobox поля
|
||
option_text: Текст опции для проверки
|
||
|
||
Returns:
|
||
bool: True если опция существует, False если нет
|
||
"""
|
||
options = self.get_combobox_options(field_name)
|
||
return option_text in options
|
||
|
||
def clear_field(self, field_name: str) -> bool:
|
||
"""
|
||
Очищает значение поля.
|
||
|
||
Args:
|
||
field_name: Название поля
|
||
|
||
Returns:
|
||
bool: True если успешно, False если нет
|
||
"""
|
||
if not self._form_fields or field_name not in self._form_fields:
|
||
return False
|
||
|
||
field_container = self._form_fields[field_name]
|
||
|
||
try:
|
||
field_container.scroll_into_view_if_needed()
|
||
self.page.wait_for_timeout(300)
|
||
|
||
# Для текстовых полей
|
||
input_field = field_container.locator("input, textarea").first
|
||
if input_field.count() > 0:
|
||
input_field.click()
|
||
self.page.wait_for_timeout(200)
|
||
input_field.fill("")
|
||
self.page.wait_for_timeout(500)
|
||
logger.debug(f"✓ Field '{field_name}' cleared")
|
||
return True
|
||
|
||
# Для combobox полей (если есть кнопка очистки)
|
||
clear_button = field_container.locator(".v-input__icon--clear, [aria-label='Clear']").first
|
||
if clear_button.count() > 0:
|
||
clear_button.click()
|
||
self.page.wait_for_timeout(500)
|
||
logger.debug(f"✓ Combobox '{field_name}' cleared")
|
||
return True
|
||
|
||
logger.debug(f"No clear method found for field '{field_name}'")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error clearing field '{field_name}': {e}")
|
||
return False
|
||
|
||
def fill_text_field(self, field_name: str, value: str) -> bool:
|
||
"""
|
||
Заполняет текстовое поле по полному совпадению названия.
|
||
|
||
Args:
|
||
field_name: Название поля
|
||
value: Значение для заполнения
|
||
|
||
Returns:
|
||
bool: True если успешно, False если нет
|
||
"""
|
||
if not self._form_fields:
|
||
logger.warning("No form fields set")
|
||
return False
|
||
|
||
# Ищем точное совпадение
|
||
if field_name not in self._form_fields:
|
||
logger.debug(f"Text field '{field_name}' not found. Available fields: {list(self._form_fields.keys())}")
|
||
return False
|
||
|
||
field_container = self._form_fields[field_name]
|
||
|
||
try:
|
||
field_container.scroll_into_view_if_needed()
|
||
# Используем wait_for_timeout из BaseComponent или добавляем небольшую задержку
|
||
self.page.wait_for_timeout(300)
|
||
|
||
# Ищем input поле
|
||
input_field = field_container.locator("input, textarea").first
|
||
if input_field.count() == 0:
|
||
logger.debug(f"Field '{field_name}' doesn't have input element")
|
||
return False
|
||
|
||
# Очищаем и заполняем
|
||
input_field.click()
|
||
self.page.wait_for_timeout(200)
|
||
input_field.fill("")
|
||
self.page.wait_for_timeout(200)
|
||
input_field.fill(value)
|
||
self.page.wait_for_timeout(500)
|
||
|
||
# Проверяем что значение установлено
|
||
actual_value = input_field.input_value()
|
||
if actual_value == value:
|
||
logger.debug(f"✓ Text field '{field_name}' filled with: '{value}'")
|
||
return True
|
||
else:
|
||
logger.warning(f"Field '{field_name}' value mismatch: expected '{value}', got '{actual_value}'")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error filling text field '{field_name}': {e}")
|
||
return False
|
||
|
||
def fill_combobox_field(self, field_name: str, value: str) -> bool:
|
||
"""
|
||
Заполняет combobox поле по полному совпадению названия.
|
||
|
||
Args:
|
||
field_name: Название поля
|
||
value: Значение для выбора
|
||
|
||
Returns:
|
||
bool: True если успешно, False если нет
|
||
"""
|
||
if not self._form_fields:
|
||
logger.warning("No form fields set")
|
||
return False
|
||
|
||
# Ищем точное совпадение
|
||
if field_name not in self._form_fields:
|
||
logger.debug(f"Combobox field '{field_name}' not found. Available fields: {list(self._form_fields.keys())}")
|
||
return False
|
||
|
||
field_container = self._form_fields[field_name]
|
||
|
||
try:
|
||
field_container.scroll_into_view_if_needed()
|
||
self.page.wait_for_timeout(300)
|
||
|
||
# Ищем кнопку открытия dropdown
|
||
dropdown_button = field_container.locator(".v-input__append-inner, [role='button']").first
|
||
|
||
if dropdown_button.count() == 0:
|
||
# Может быть поле уже открыто
|
||
input_field = field_container.locator("input").first
|
||
input_field.click()
|
||
self.page.wait_for_timeout(1000)
|
||
else:
|
||
dropdown_button.click()
|
||
self.page.wait_for_timeout(1000)
|
||
|
||
# Ищем выпадающий список
|
||
active_menu = None
|
||
menu_selectors = [
|
||
".v-menu__content.menuable__content__active",
|
||
".v-select__menu",
|
||
".v-autocomplete__content",
|
||
".v-menu__content"
|
||
]
|
||
|
||
for selector in menu_selectors:
|
||
menu = self.page.locator(selector).first
|
||
if menu.count() > 0 and menu.is_visible():
|
||
active_menu = menu
|
||
break
|
||
|
||
if not active_menu:
|
||
logger.debug(f"No dropdown menu found for '{field_name}'")
|
||
return False
|
||
|
||
# Ищем нужный элемент
|
||
dropdown_item = active_menu.locator(f"div[role='listitem'], .v-list-item").filter(
|
||
has_text=value
|
||
).first
|
||
|
||
if dropdown_item.count() == 0:
|
||
logger.debug(f"Value '{value}' not found in dropdown for '{field_name}'")
|
||
self.page.keyboard.press("Escape")
|
||
return False
|
||
|
||
# Выбираем значение
|
||
dropdown_item.click()
|
||
logger.debug(f"✓ Combobox '{field_name}' set to: '{value}'")
|
||
self.page.wait_for_timeout(1000)
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error filling combobox '{field_name}': {e}")
|
||
self.page.keyboard.press("Escape")
|
||
return False
|