"""Базовый модуль для работы с формами стойки.""" 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