e-nms_qa_automation/components/form_field_component.py

645 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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