refactor: унификация работы с формами создания и редактирования стоек
- Добавлен базовый класс BaseRackForm с общей логикой для работы с формамиra6/create_rack
parent
b024fac0d8
commit
f075024386
|
|
@ -0,0 +1,469 @@
|
|||
"""Базовый модуль для работы с формами стойки."""
|
||||
|
||||
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
|
||||
Loading…
Reference in New Issue