"""Модуль для работы с формой создания стойки.""" import time from dataclasses import dataclass from typing import List, Dict, Any from playwright.sync_api import Page from tools.logger import get_logger from locators.rack_locators import RackLocators from elements.text_input_element import TextInput from components.base_component import BaseComponent from components.dropdown_list_component import DropdownList logger = get_logger("CREATE_RACK_FORM") logger.setLevel("INFO") @dataclass class CreateRackData: """Класс для хранения данных создаваемой стойки.""" # Основные поля 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 CreateRackForm(BaseComponent): """Компонент для работы с формой создания стойки.""" # Маппинг текстовых полей TEXT_FIELDS_MAPPING = { "Имя": ("name", "name_input"), "Комментарий": ("comment", "comment_input"), "Серийный номер": ("serial", "serial_input"), "Инвентарный номер": ("inventory", "inventory_input"), } # Маппинг полей для заполнения combobox полей COMBOBOX_FIELDS_MAPPING = { "Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"), "Состояние": ("state", "state_input", "state_list"), "Высота в юнитах": ("usize", "usize_input", "usize_list"), "Глубина (мм)": ("depth", "depth_input", "depth_list"), "Владелец": ("owner", "owner_input", "owner_list"), "Обслуживающая организация": ("service_org", "service_input", "service_list"), "Проект/Титул": ("project", "project_input", "project_list") } # Локаторы для текстовых полей (из RackLocators) TEXT_FIELDS_LOCATORS = { "Имя": RackLocators.CREATE_RACK_FORM_FIELD_NAME, "Комментарий": RackLocators.CREATE_RACK_FORM_FIELD_COMMENT, "Серийный номер": RackLocators.CREATE_RACK_FORM_FIELD_SERIAL, "Инвентарный номер": RackLocators.CREATE_RACK_FORM_FIELD_INVENTORY, } # Локаторы для combobox полей (из RackLocators) COMBOBOX_FIELDS_LOCATORS = { "Высота в юнитах": RackLocators.CREATE_RACK_FORM_SELECT_USIZE, "Глубина (мм)": RackLocators.CREATE_RACK_FORM_SELECT_DEPTH, "Ввод кабеля": RackLocators.CREATE_RACK_FORM_SELECT_CABLE_INPUT, "Состояние": RackLocators.CREATE_RACK_FORM_SELECT_CONDITION_TYPE, "Владелец": RackLocators.CREATE_RACK_FORM_SELECT_OWNER, "Обслуживающая организация": RackLocators.CREATE_RACK_FORM_SELECT_SERVICE_PROVIDER, "Проект/Титул": RackLocators.CREATE_RACK_FORM_SELECT_PROJECT, } def __init__(self, page: Page) -> None: """Инициализирует компонент формы создания стойки. Args: page: Экземпляр страницы Playwright """ super().__init__(page) self.page = page self.content_items = {} self.available_fields = None # Инициализация полей формы self._init_form_fields() def _init_form_fields(self) -> None: """Инициализирует все поля формы создания.""" # Получаем доступные поля формы container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER).nth(1) self.available_fields = self.get_input_fields_locators(container_locator) self._init_text_fields() self._init_combobox_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: """Инициализирует одно текстовое поле. Args: field_label: Метка поля locator: Локатор поля widget_name: Имя виджета """ try: element = self.page.locator(locator).first if element.count() > 0 and element.is_visible(): # Создаем TextInput для поля 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 поле. Args: field_label: Метка поля locator: Локатор поля input_name: Имя поля ввода list_name: Имя списка """ try: element = self.page.locator(locator).first if element.count() > 0 and element.is_visible(): # Для combobox создаем TextInput для клика field_input = TextInput(self.page, element, input_name) self.content_items[input_name] = field_input # Добавляем DropdownList для выбора значений 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 clear_field(self, field_name: str) -> None: """Очищает указанное поле. Args: field_name: Название поля для очистки """ logger.debug(f"Clearing field: '{field_name}'") # Получаем локатор поля locator = None if field_name in self.COMBOBOX_FIELDS_LOCATORS: locator = self.COMBOBOX_FIELDS_LOCATORS[field_name] elif field_name in self.TEXT_FIELDS_LOCATORS: locator = self.TEXT_FIELDS_LOCATORS[field_name] else: 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: 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}") return # Для combobox полей if field_name in self.COMBOBOX_FIELDS_LOCATORS: # Поднимаемся до родительского контейнера 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}'") def get_content_item(self, item_name: str) -> Any: """Возвращает элемент контента по имени. Args: item_name: Имя элемента Returns: Элемент или None если не найден """ return self.content_items.get(item_name) def _scroll_to_element_in_dropdown(self, value: str) -> bool: """Скроллит выпадающий список до элемента с нужным текстом используя playwright. Args: value: Текст для поиска Returns: bool: True если элемент найден, False в противном случае """ 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 last_item_text = "" 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.debug(f"Scroll attempt {attempts}/{max_attempts}") logger.warning(f"Element with text '{value}' not found after {max_attempts} scroll attempts") return False def fill_rack_data(self, rack_data: CreateRackData) -> Dict[str, int]: """Заполняет поля формы создания стойки. Args: rack_data: Данные для заполнения Returns: Словарь с результатами заполнения """ results = { "text_fields_filled": 0, "combobox_fields_filled": 0, } self._fill_text_fields(rack_data, results) self._fill_combobox_fields(rack_data, results) logger.info(f"Filled {results['text_fields_filled']} text fields and " f"{results['combobox_fields_filled']} combobox fields") return results def _fill_text_fields(self, rack_data: CreateRackData, results: Dict[str, int]) -> None: """Заполняет текстовые поля. Args: rack_data: Данные для заполнения results: Словарь с результатами """ 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: """Заполняет одно текстовое поле. Args: field_label: Метка поля field_name: Имя поля value: Значение для заполнения results: Словарь с результатами """ 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: CreateRackData, results: Dict[str, int]) -> None: """Заполняет combobox поля. Args: rack_data: Данные для заполнения results: Словарь с результатами """ 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 поле. Args: field_label: Метка поля input_name: Имя поля ввода list_name: Имя списка value: Значение для выбора results: Словарь с результатами """ 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 = 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 if 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 is_field_highlighted_as_error(self, field_name: str) -> bool: """Проверяет, подсвечено ли поле как ошибочное. Args: field_name: Название поля для проверки Returns: bool: True если поле подсвечено ошибкой, False в противном случае """ # Проверяем в текстовых полях if field_name in self.TEXT_FIELDS_LOCATORS: locator = self.TEXT_FIELDS_LOCATORS[field_name] field_element = self.page.locator(locator).first if field_element.count() == 0: logger.debug(f"Field '{field_name}' not found") return False # Поднимаемся до родительского контейнера с классом v-input 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}, classes: {class_attr}") return is_error # Проверяем в combobox полях elif field_name in self.COMBOBOX_FIELDS_LOCATORS: locator = self.COMBOBOX_FIELDS_LOCATORS[field_name] field_element = self.page.locator(locator).first if field_element.count() == 0: logger.debug(f"Field '{field_name}' not found") return False # Поднимаемся до родительского контейнера с классом v-input 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}, classes: {class_attr}") return is_error return False def verify_required_fields_highlighted(self, field_names: List[str]) -> Dict[str, bool]: """Проверяет, что указанные поля подсвечены как обязательные (с ошибкой). Args: field_names: Список названий полей для проверки Returns: Словарь с результатами проверки {field_name: is_highlighted} """ 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: """Ожидает появления подсветки ошибки на поле. Args: field_name: Название поля timeout: Таймаут в миллисекундах Returns: bool: True если ошибка появилась, 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