"""Модуль для работы с полями формы.""" 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