470 lines
21 KiB
Python
470 lines
21 KiB
Python
"""Базовый модуль для работы с формами стойки."""
|
||
|
||
import time
|
||
from dataclasses import dataclass
|
||
from typing import Optional, List, Dict, Any, Tuple
|
||
from abc import ABC, abstractmethod
|
||
from playwright.sync_api import Page
|
||
from tools.logger import get_logger
|
||
from elements.text_input_element import TextInput
|
||
from components.base_component import BaseComponent
|
||
from components.dropdown_list_component import DropdownList
|
||
|
||
logger = get_logger("BASE_RACK_FORM")
|
||
logger.setLevel("INFO")
|
||
|
||
@dataclass
|
||
class BaseRackData:
|
||
"""Базовый класс для хранения данных стойки."""
|
||
|
||
# Основные поля
|
||
name: str = ""
|
||
serial: str = ""
|
||
inventory: str = ""
|
||
comment: str = ""
|
||
|
||
# Combobox поля
|
||
cable_entry: str = ""
|
||
state: str = ""
|
||
depth: str = ""
|
||
usize: str = ""
|
||
|
||
# Combobox поля (справочники)
|
||
owner: str = ""
|
||
service_org: str = ""
|
||
project: str = ""
|
||
|
||
|
||
class BaseRackForm(BaseComponent, ABC):
|
||
"""Базовый компонент для работы с формами стойки."""
|
||
|
||
# Маппинг текстовых полей (должен быть переопределен в наследниках)
|
||
TEXT_FIELDS_MAPPING: Dict[str, Tuple[str, str]] = {}
|
||
TEXT_FIELDS_LOCATORS: Dict[str, str] = {}
|
||
|
||
# Маппинг combobox полей (должен быть переопределен в наследниках)
|
||
COMBOBOX_FIELDS_MAPPING: Dict[str, Tuple[str, str, str]] = {}
|
||
COMBOBOX_FIELDS_LOCATORS: Dict[str, str] = {}
|
||
|
||
# Дополнительные типы полей (checkbox и т.д.) - опционально
|
||
CHECKBOX_FIELDS_MAPPING: Dict[str, Tuple[str, str]] = {}
|
||
CHECKBOX_FIELDS_LOCATORS: Dict[str, str] = {}
|
||
|
||
def __init__(self, page: Page, form_container_locator: str) -> None:
|
||
"""Инициализирует базовый компонент формы стойки.
|
||
|
||
Args:
|
||
page: Экземпляр страницы Playwright
|
||
form_container_locator: Локатор контейнера формы
|
||
"""
|
||
super().__init__(page)
|
||
self.page = page
|
||
self.form_container_locator = form_container_locator
|
||
self.content_items: Dict[str, Any] = {}
|
||
self.available_fields = None
|
||
|
||
# Инициализация полей формы
|
||
self._init_form_fields()
|
||
|
||
def _init_form_fields(self) -> None:
|
||
"""Инициализирует все поля формы."""
|
||
container_locator = self.page.locator(self.form_container_locator)
|
||
if container_locator.count() > 0:
|
||
self.available_fields = self.get_input_fields_locators(container_locator)
|
||
|
||
self._init_text_fields()
|
||
self._init_combobox_fields()
|
||
self._init_checkbox_fields()
|
||
|
||
def _init_text_fields(self) -> None:
|
||
"""Инициализирует текстовые поля формы."""
|
||
for field_label, (attr_name, widget_name) in self.TEXT_FIELDS_MAPPING.items():
|
||
locator = self.TEXT_FIELDS_LOCATORS.get(field_label)
|
||
if not locator:
|
||
continue
|
||
self._init_single_text_field(field_label, locator, widget_name)
|
||
|
||
def _init_single_text_field(self, field_label: str, locator: str, widget_name: str) -> None:
|
||
"""Инициализирует одно текстовое поле."""
|
||
try:
|
||
element = self.page.locator(locator).first
|
||
if element.count() > 0 and element.is_visible():
|
||
field_input = TextInput(self.page, element, widget_name)
|
||
self.content_items[widget_name] = field_input
|
||
logger.debug(f"Initialized text field: '{field_label}'")
|
||
except Exception as e:
|
||
logger.error(f"Error initializing text field '{field_label}': {e}")
|
||
|
||
def _init_combobox_fields(self) -> None:
|
||
"""Инициализирует combobox поля формы."""
|
||
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
|
||
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label)
|
||
if not locator:
|
||
continue
|
||
self._init_single_combobox_field(field_label, locator, input_name, list_name)
|
||
|
||
def _init_single_combobox_field(
|
||
self, field_label: str, locator: str, input_name: str, list_name: str
|
||
) -> None:
|
||
"""Инициализирует одно combobox поле."""
|
||
try:
|
||
element = self.page.locator(locator).first
|
||
if element.count() > 0 and element.is_visible():
|
||
field_input = TextInput(self.page, element, input_name)
|
||
self.content_items[input_name] = field_input
|
||
self.content_items[list_name] = DropdownList(self.page)
|
||
logger.debug(f"Initialized combobox field: '{field_label}'")
|
||
except Exception as e:
|
||
logger.error(f"Error initializing combobox field '{field_label}': {e}")
|
||
|
||
def _init_checkbox_fields(self) -> None:
|
||
"""Инициализирует checkbox поля формы (опционально)."""
|
||
if not self.CHECKBOX_FIELDS_MAPPING:
|
||
return
|
||
|
||
for field_label, (attr_name, widget_name) in self.CHECKBOX_FIELDS_MAPPING.items():
|
||
locator = self.CHECKBOX_FIELDS_LOCATORS.get(field_label)
|
||
if not locator:
|
||
continue
|
||
self._init_single_checkbox_field(field_label, locator, widget_name)
|
||
|
||
def _init_single_checkbox_field(self, field_label: str, locator: str, widget_name: str) -> None:
|
||
"""Инициализирует одно checkbox поле."""
|
||
try:
|
||
checkbox_input = self.page.locator(locator).first
|
||
if checkbox_input.count() == 0:
|
||
logger.debug(f"Checkbox '{field_label}' not found")
|
||
return
|
||
|
||
# Импортируем здесь чтобы избежать циклических импортов
|
||
from elements.checkbox_element import Checkbox
|
||
|
||
checkbox = Checkbox(self.page, checkbox_input, widget_name)
|
||
self.content_items[widget_name] = checkbox
|
||
logger.debug(f"Initialized checkbox field: '{field_label}'")
|
||
except Exception as e:
|
||
logger.error(f"Error initializing checkbox '{field_label}': {e}")
|
||
|
||
def get_content_item(self, item_name: str) -> Any:
|
||
"""Возвращает элемент контента по имени."""
|
||
return self.content_items.get(item_name)
|
||
|
||
def clear_field(self, field_name: str) -> None:
|
||
"""Очищает указанное поле."""
|
||
logger.debug(f"Clearing field: '{field_name}'")
|
||
|
||
# Проверяем, не является ли поле чекбоксом
|
||
if field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||
logger.debug(f"Field '{field_name}' is a checkbox, skipping clear operation")
|
||
return
|
||
|
||
# Получаем локатор поля
|
||
locator = self._get_field_locator(field_name)
|
||
if not locator:
|
||
logger.warning(f"Unknown field: {field_name}")
|
||
return
|
||
|
||
field_element = self.page.locator(locator).first
|
||
if field_element.count() == 0:
|
||
logger.debug(f"Field '{field_name}' not found")
|
||
return
|
||
|
||
# Очистка в зависимости от типа поля
|
||
if field_name in self.TEXT_FIELDS_LOCATORS:
|
||
self._clear_text_field(field_element, field_name)
|
||
elif field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
||
self._clear_combobox_field(field_element, field_name)
|
||
|
||
def _get_field_locator(self, field_name: str) -> Optional[str]:
|
||
"""Получает локатор поля по его названию."""
|
||
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
|
||
return self.COMBOBOX_FIELDS_LOCATORS[field_name]
|
||
elif field_name in self.TEXT_FIELDS_LOCATORS:
|
||
return self.TEXT_FIELDS_LOCATORS[field_name]
|
||
elif field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||
return self.CHECKBOX_FIELDS_LOCATORS[field_name]
|
||
return None
|
||
|
||
def _clear_text_field(self, field_element, field_name: str) -> None:
|
||
"""Очищает текстовое поле."""
|
||
try:
|
||
field_element.click()
|
||
field_element.page.keyboard.press("Control+A")
|
||
field_element.page.keyboard.press("Backspace")
|
||
self.wait_for_timeout(200)
|
||
logger.debug(f"Text field '{field_name}' cleared")
|
||
except Exception as e:
|
||
logger.debug(f"Could not clear text field '{field_name}': {e}")
|
||
|
||
def _clear_combobox_field(self, field_element, field_name: str) -> None:
|
||
"""Очищает combobox поле."""
|
||
try:
|
||
parent_container = field_element.locator(
|
||
"xpath=ancestor::div[contains(@class, 'v-input')]"
|
||
).first
|
||
|
||
if parent_container.count() == 0:
|
||
logger.debug(f"Parent container not found for field '{field_name}'")
|
||
return
|
||
|
||
clear_button = parent_container.locator(
|
||
".v-input__icon--clear button, .v-input__icon--append button, i.mdi-close-circle, i.mdi-close"
|
||
).first
|
||
|
||
if clear_button.count() > 0 and clear_button.is_visible():
|
||
clear_button.click()
|
||
self.wait_for_timeout(300)
|
||
logger.debug(f"Combobox field '{field_name}' cleared")
|
||
else:
|
||
logger.debug(f"Clear button not found for field '{field_name}'")
|
||
except Exception as e:
|
||
logger.debug(f"Error clearing combobox field '{field_name}': {e}")
|
||
|
||
def _scroll_to_element_in_dropdown(self, value: str) -> bool:
|
||
"""Скроллит выпадающий список до элемента с нужным текстом."""
|
||
logger.debug(f"Scrolling to find element with text: '{value}'")
|
||
|
||
dropdown_menu = self.page.locator("div.menuable__content__active").first
|
||
if dropdown_menu.count() == 0:
|
||
logger.error("Active dropdown menu not found")
|
||
return False
|
||
|
||
max_attempts = 10
|
||
attempts = 0
|
||
|
||
while attempts < max_attempts:
|
||
visible_items = dropdown_menu.locator("a.v-list__tile, div[role='listitem']").all()
|
||
|
||
if visible_items:
|
||
for item in visible_items:
|
||
item_text = item.text_content() or ""
|
||
if value in item_text:
|
||
logger.debug(f"Found element with text '{value}'")
|
||
item.scroll_into_view_if_needed()
|
||
self.wait_for_timeout(300)
|
||
return True
|
||
|
||
last_item = visible_items[-1]
|
||
last_item_text = last_item.text_content() or ""
|
||
logger.debug(f"Scrolling to last visible item: '{last_item_text}'")
|
||
last_item.scroll_into_view_if_needed()
|
||
self.wait_for_timeout(500)
|
||
else:
|
||
dropdown_menu.evaluate("(el) => el.scrollTop += 200")
|
||
self.wait_for_timeout(300)
|
||
|
||
attempts += 1
|
||
|
||
logger.warning(f"Element with text '{value}' not found after {max_attempts} attempts")
|
||
return False
|
||
|
||
def _fill_text_fields(self, rack_data: BaseRackData, results: Dict[str, int]) -> None:
|
||
"""Заполняет текстовые поля."""
|
||
for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items():
|
||
value = getattr(rack_data, attr_name, "")
|
||
if not value or not str(value).strip():
|
||
continue
|
||
self._fill_single_text_field(field_label, field_name, value, results)
|
||
|
||
def _fill_single_text_field(
|
||
self, field_label: str, field_name: str, value: str, results: Dict[str, int]
|
||
) -> None:
|
||
"""Заполняет одно текстовое поле."""
|
||
try:
|
||
input_field = self.get_content_item(field_name)
|
||
if input_field:
|
||
input_field.input_value(value)
|
||
results["text_fields_filled"] += 1
|
||
logger.debug(f"Field '{field_label}' filled: '{value}'")
|
||
except Exception as e:
|
||
logger.error(f"Error filling field '{field_label}': {e}")
|
||
|
||
def _fill_combobox_fields(self, rack_data: BaseRackData, results: Dict[str, int]) -> None:
|
||
"""Заполняет combobox поля."""
|
||
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
|
||
value = getattr(rack_data, attr_name, "")
|
||
if not value or not str(value).strip():
|
||
continue
|
||
self._fill_single_combobox_field(field_label, input_name, list_name, value, results)
|
||
|
||
def _fill_single_combobox_field(
|
||
self, field_label: str, input_name: str, list_name: str, value: str, results: Dict[str, int]
|
||
) -> None:
|
||
"""Заполняет одно combobox поле."""
|
||
try:
|
||
combobox_field = self.get_content_item(input_name)
|
||
if not combobox_field:
|
||
logger.warning(f"Field '{field_label}' input not found")
|
||
return
|
||
|
||
combobox_field.click(force=True)
|
||
self.wait_for_timeout(1000)
|
||
|
||
if not self._scroll_to_element_in_dropdown(value):
|
||
logger.error(f"Could not find element with text '{value}' after scrolling")
|
||
self.page.mouse.click(10, 10)
|
||
self.wait_for_timeout(300)
|
||
return
|
||
|
||
dropdown_menu = self.page.locator("div.menuable__content__active").first
|
||
item_locator = self._find_dropdown_item(dropdown_menu, value)
|
||
|
||
if item_locator and item_locator.count() > 0:
|
||
item_locator.scroll_into_view_if_needed()
|
||
self.wait_for_timeout(300)
|
||
item_locator.click()
|
||
results["combobox_fields_filled"] += 1
|
||
logger.debug(f"Field '{field_label}' set: '{value}'")
|
||
self.wait_for_timeout(500)
|
||
else:
|
||
logger.error(f"Item with text '{value}' not found in dropdown for field '{field_label}'")
|
||
self.page.mouse.click(10, 10)
|
||
self.wait_for_timeout(300)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error filling combobox '{field_label}': {e}")
|
||
self.page.mouse.click(10, 10)
|
||
|
||
def _find_dropdown_item(self, dropdown_menu, value: str):
|
||
"""Находит элемент в выпадающем списке."""
|
||
item_locator = dropdown_menu.locator(f"a.v-list__tile:has-text('{value}')").first
|
||
if item_locator.count() == 0:
|
||
item_locator = dropdown_menu.locator(f"span:has-text('{value}')").first
|
||
if item_locator.count() == 0:
|
||
item_locator = dropdown_menu.locator(f"div[role='listitem']:has-text('{value}')").first
|
||
return item_locator
|
||
|
||
def _fill_checkbox_fields(self, rack_data: BaseRackData, results: Dict[str, int]) -> None:
|
||
"""Заполняет checkbox поля (опционально)."""
|
||
if not hasattr(self, 'CHECKBOX_FIELDS_MAPPING'):
|
||
return
|
||
|
||
for field_label, (attr_name, widget_name) in self.CHECKBOX_FIELDS_MAPPING.items():
|
||
value = getattr(rack_data, attr_name, None)
|
||
if value is None:
|
||
continue
|
||
self._fill_single_checkbox_field(field_label, widget_name, value, results)
|
||
|
||
def _fill_single_checkbox_field(
|
||
self, field_label: str, widget_name: str, value: bool, results: Dict[str, int]
|
||
) -> None:
|
||
"""Заполняет одно checkbox поле."""
|
||
try:
|
||
checkbox = self.get_content_item(widget_name)
|
||
if not checkbox:
|
||
logger.warning(f"Checkbox '{field_label}' not found")
|
||
return
|
||
|
||
if value:
|
||
checkbox.check(force=True)
|
||
logger.debug(f"Checkbox '{field_label}' checked")
|
||
else:
|
||
checkbox.uncheck(force=True)
|
||
logger.debug(f"Checkbox '{field_label}' unchecked")
|
||
|
||
results["checkboxes_set"] += 1
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error setting checkbox '{field_label}': {e}")
|
||
|
||
@abstractmethod
|
||
def fill_rack_data(self, rack_data: BaseRackData) -> Dict[str, int]:
|
||
"""Абстрактный метод для заполнения данных стойки."""
|
||
pass
|
||
|
||
def is_field_highlighted_as_error(self, field_name: str) -> bool:
|
||
"""Проверяет, подсвечено ли поле как ошибочное."""
|
||
# Для чекбоксов не проверяем ошибки
|
||
if field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||
return False
|
||
|
||
locator = self._get_field_locator(field_name)
|
||
if not locator:
|
||
return False
|
||
|
||
field_element = self.page.locator(locator).first
|
||
if field_element.count() == 0:
|
||
logger.debug(f"Field '{field_name}' not found")
|
||
return False
|
||
|
||
parent_input = field_element.locator(
|
||
"xpath=ancestor::div[contains(@class, 'v-input')]"
|
||
).first
|
||
|
||
if parent_input.count() > 0:
|
||
class_attr = parent_input.get_attribute("class") or ""
|
||
is_error = "v-input--error" in class_attr or "error--text" in class_attr
|
||
logger.debug(f"Field '{field_name}' error state: {is_error}")
|
||
return is_error
|
||
|
||
return False
|
||
|
||
def verify_required_fields_highlighted(self, field_names: List[str]) -> Dict[str, bool]:
|
||
"""Проверяет, что указанные поля подсвечены как обязательные."""
|
||
results = {}
|
||
for field_name in field_names:
|
||
results[field_name] = self.is_field_highlighted_as_error(field_name)
|
||
logger.debug(f"Field '{field_name}' highlighted: {results[field_name]}")
|
||
return results
|
||
|
||
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
|
||
"""Ожидает появления подсветки ошибки на поле."""
|
||
if field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||
return False
|
||
|
||
start_time = time.time()
|
||
while (time.time() - start_time) * 1000 < timeout:
|
||
if self.is_field_highlighted_as_error(field_name):
|
||
return True
|
||
self.wait_for_timeout(200)
|
||
return False
|
||
|
||
def get_field_value(self, field_name: str) -> Optional[str]:
|
||
"""Получает значение поля."""
|
||
# Для чекбоксов
|
||
if field_name in self.CHECKBOX_FIELDS_LOCATORS:
|
||
for field_label, (attr_name, widget_name) in self.CHECKBOX_FIELDS_MAPPING.items():
|
||
if attr_name == field_name or field_label == field_name:
|
||
checkbox = self.get_content_item(widget_name)
|
||
if checkbox:
|
||
return str(checkbox.is_checked())
|
||
return None
|
||
|
||
# Для текстовых полей
|
||
if field_name in self.TEXT_FIELDS_LOCATORS:
|
||
for field_label, (attr_name, widget_name) in self.TEXT_FIELDS_MAPPING.items():
|
||
if attr_name == field_name or field_label == field_name:
|
||
input_field = self.get_content_item(widget_name)
|
||
if input_field:
|
||
return input_field.get_input_value()
|
||
return None
|
||
|
||
# Для combobox полей
|
||
return self._get_combobox_value(field_name)
|
||
|
||
def _get_combobox_value(self, field_name: str) -> Optional[str]:
|
||
"""Получает значение combobox поля."""
|
||
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_name)
|
||
if not locator:
|
||
for field_label, (attr_name, input_name, _) in self.COMBOBOX_FIELDS_MAPPING.items():
|
||
if attr_name == field_name or field_label == field_name:
|
||
input_field = self.get_content_item(input_name)
|
||
if input_field:
|
||
selections = input_field.element.locator(
|
||
"xpath=ancestor::div[contains(@class, 'v-select__selections')]"
|
||
).first
|
||
if selections.count() > 0:
|
||
value_span = selections.locator("span").first
|
||
return value_span.text_content() or ""
|
||
return None
|
||
|
||
element = self.page.locator(locator).first
|
||
if element.count() > 0:
|
||
selections = element.locator(
|
||
"xpath=ancestor::div[contains(@class, 'v-select__selections')]"
|
||
).first
|
||
if selections.count() > 0:
|
||
value_span = selections.locator("span").first
|
||
return value_span.text_content() or ""
|
||
return None
|