diff --git a/components_derived/modal_edit_rack.py b/components_derived/modal_edit_rack.py new file mode 100644 index 0000000..00d3509 --- /dev/null +++ b/components_derived/modal_edit_rack.py @@ -0,0 +1,1401 @@ +"""Модуль для работы с модальным окном редактирования стойки.""" + +import re +from dataclasses import dataclass +from typing import Optional, List, Tuple, 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 elements.text_element import Text +from elements.checkbox_element import Checkbox +from components.modal_window_component import ModalWindowComponent +from components.dropdown_list_component import DropdownList +from components.confirm_component import ConfirmComponent + + +logger = get_logger("MODAL_EDIT_RACK") +logger.setLevel("INFO") + +@dataclass +class RackEditData: + """Класс для хранения данных редактирования стойки. + + Содержит все возможные поля, которые могут быть изменены + в модальном окне редактирования стойки. + """ + + # Основные поля (редактируемые) + name: str = "" + serial: str = "" + inventory: str = "" + comment: str = "" + allocated_power: str = "" + + # Combobox поля (редактируемые) + cable_entry: str = "" + state: str = "" + depth: str = "" + usize: str = "" + owner: str = "" + service_org: str = "" + project: str = "" + + # Checkbox поля (редактируемые) + ventilation_panel: Optional[bool] = None + + # Правила доступа + read_access_rules: str = "" + write_access_rules: str = "" + sms_access_rules: str = "" + email_access_rules: str = "" + push_access_rules: str = "" + + +class ModalEditRack(ModalWindowComponent): + """Компонент для работы с модальным окном редактирования стойки. + + Предоставляет методы для взаимодействия с элементами окна: + - переключение между вкладками + - заполнение полей общей информации + - работа с изображениями + - настройка правил доступа + - сохранение/отмена изменений + - удаление стойки + """ + + # Константы для названий вкладок + TAB_GENERAL = "Общая информация" + TAB_IMAGE = "Изображение" + TAB_SETTINGS = "Настройки" + + # Маппинг полей для заполнения текстовых полей + TEXT_FIELDS_MAPPING = { + "Имя": ("name", "name_input"), + "Комментарий": ("comment", "comment_input"), + "Серийный номер": ("serial", "serial_input"), + "Инвентарный номер": ("inventory", "inventory_input"), + "Выделенная мощность (Вт/ВА)": ("allocated_power", "power_input"), + } + + # Маппинг полей для заполнения combobox полей + COMBOBOX_FIELDS_MAPPING = { + "Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"), + "Состояние": ("state", "state_input", "state_list"), + "Глубина (мм)": ("depth", "depth_input", "depth_list"), + "Высота в юнитах": ("usize", "usize_input", "usize_list"), + "Владелец": ("owner", "owner_input", "owner_list"), + "Обслуживающая организация": ("service_org", "service_input", "service_list"), + "Проект/Титул": ("project", "project_input", "project_list") + } + + # Локаторы для текстовых полей (из RackLocators) + TEXT_FIELDS_LOCATORS = { + "Имя": RackLocators.INPUT_FORM_RACK_DATA_FIELD_NAME, + "Комментарий": RackLocators.INPUT_FORM_RACK_DATA_FIELD_COMMENT, + "Серийный номер": RackLocators.INPUT_FORM_RACK_DATA_FIELD_SERIAL, + "Инвентарный номер": RackLocators.INPUT_FORM_RACK_DATA_FIELD_INVENTORY, + "Выделенная мощность (Вт/ВА)": RackLocators.INPUT_FORM_RACK_DATA_FIELD_POWER, + } + + # Локаторы для combobox полей (из RackLocators) + COMBOBOX_FIELDS_LOCATORS = { + "Ввод кабеля": RackLocators.INPUT_FORM_RACK_DATA_FIELD_CABLE_ENTRY, + "Состояние": RackLocators.INPUT_FORM_RACK_DATA_FIELD_CONDITION_TYPE, + "Глубина (мм)": RackLocators.INPUT_FORM_RACK_DATA_FIELD_DEPTH, + "Высота в юнитах": RackLocators.INPUT_FORM_RACK_DATA_FIELD_USIZE, + "Владелец": RackLocators.INPUT_FORM_RACK_DATA_FIELD_OWNER, + "Обслуживающая организация": RackLocators.INPUT_FORM_RACK_DATA_FIELD_SERVICE_PROVIDER, + "Проект/Титул": RackLocators.INPUT_FORM_RACK_DATA_FIELD_PROJECT, + } + + # Маппинг полей для вкладки "Настройки" + ACCESS_RULES_MAPPING = { + "Правила доступа для чтения": ( + "read_access_rules", "rules_read_input", "rules_read_list" + ), + "Правила доступа для записи": ( + "write_access_rules", "rules_write_input", "rules_write_list" + ), + "Правила доступа для получения СМС": ( + "sms_access_rules", "rules_sms_input", "rules_sms_list" + ), + "Правила доступа для получения email сообщения": ( + "email_access_rules", "rules_email_input", "rules_email_list" + ), + "Правила доступа для получения push уведомлений": ( + "push_access_rules", "rules_push_input", "rules_push_list" + ), + } + + # Локаторы для полей правил доступа (из RackLocators) + ACCESS_RULES_LOCATORS = { + "Правила доступа для чтения": RackLocators.SETTINGS_READ_RULES, + "Правила доступа для записи": RackLocators.SETTINGS_WRITE_RULES, + "Правила доступа для получения СМС": RackLocators.SETTINGS_SMS_RULES, + "Правила доступа для получения email сообщения": RackLocators.SETTINGS_EMAIL_RULES, + "Правила доступа для получения push уведомлений": RackLocators.SETTINGS_PUSH_RULES, + } + + def __init__(self, page: Page, rack_name: str) -> None: + """Инициализирует компонент редактирования стойки. + + Args: + page: Экземпляр страницы Playwright. + rack_name: Имя редактируемой стойки. + """ + + super().__init__(page) + self.rack_name = rack_name + self.page = page + self.available_fields = None + self.active_tab = self.TAB_GENERAL + self.tabs = {} + self.content_items = {} + self.delete_confirm = None + + # Настройка заголовка и кнопки закрытия + self.window_title = rack_name + locator_button_toolbar_close = ( + self.page.get_by_role("navigation") + .filter(has_text=re.compile(self.window_title)) + .get_by_role("button") + ) + + self.add_toolbar_title(self.window_title) + self.add_toolbar_button(locator_button_toolbar_close, "close") + + # Инициализация компонента подтверждения удаления + self.delete_confirm = ConfirmComponent(page, " Отмена ", " Удалить ") + + # Инициализация вкладок и содержимого + self._init_tabs() + self._init_active_tab_content() + self._init_toolbar_buttons() + + def _init_tabs(self) -> None: + """Инициализирует вкладки окна редактирования.""" + + self.tabs = { + self.TAB_GENERAL: self.page.locator(RackLocators.MODAL_TAB_GENERAL), + self.TAB_IMAGE: self.page.locator(RackLocators.MODAL_TAB_IMAGE), + self.TAB_SETTINGS: self.page.locator(RackLocators.MODAL_TAB_SETTINGS), + } + logger.debug(f"Initialized tabs: {list(self.tabs.keys())}") + + def _init_active_tab_content(self) -> None: + """Инициализирует содержимое активной вкладки.""" + + self.content_items = {} + + if self.active_tab == self.TAB_GENERAL: + self._init_general_tab_content() + elif self.active_tab == self.TAB_IMAGE: + self._init_image_tab_content() + else: + self._init_settings_tab_content() + + def _init_general_tab_content(self) -> None: + """Инициализирует содержимое вкладки 'Общая информация'.""" + + # Получаем доступные поля формы с помощью базового метода + self.available_fields = self.get_input_fields_locators( + self.page.locator(RackLocators.INPUT_FORM_RACK_DATA)) + + self._init_text_fields() + self._init_combobox_fields() + self._init_checkbox_fields() + + def _init_text_fields(self) -> None: + """Инициализирует текстовые поля формы.""" + + for field_label, (_, 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(): + field_input = TextInput(self.page, element, widget_name) + self.add_content_item(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, (_, 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(): + field_input = TextInput(self.page, element, input_name) + self.add_content_item(input_name, field_input) + self.add_content_item(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 поля формы.""" + + try: + self._init_ventilation_checkbox() + except Exception as e: + logger.error(f"Error initializing checkbox: {e}") + + def _init_ventilation_checkbox(self) -> None: + """Инициализирует чекбокс вентиляционной панели.""" + + checkbox_input = self.page.locator( + RackLocators.INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION + ).first + + if checkbox_input.count() == 0: + return + + checkbox = Checkbox(self.page, checkbox_input, "ventilation_panel") + self.add_content_item("ventilation_checkbox", checkbox) + + label_locator = self.page.locator("label:has-text('Вентиляционная панель')").first + if label_locator.count() > 0: + label_text = Text(self.page, label_locator, "ventilation_checkbox_label") + self.add_content_item("ventilation_checkbox_label", label_text) + + logger.debug("Initialized ventilation panel checkbox") + + def _init_image_tab_content(self) -> None: + """Инициализирует содержимое вкладки 'Изображение'.""" + + try: + self._init_image_upload_elements() + logger.debug("Image tab content initialized") + except Exception as e: + logger.error(f"Error initializing image tab content: {e}") + + def _init_image_upload_elements(self) -> None: + """Инициализирует элементы загрузки изображения.""" + + image_tab_container = self.page.locator(RackLocators.IMAGE_UPLOAD_CONTAINER) + upload_icon = image_tab_container.locator(RackLocators.IMAGE_UPLOAD_ICON) + self.add_content_item("image_upload_icon", upload_icon) + + upload_input = image_tab_container.locator(RackLocators.IMAGE_UPLOAD_INPUT) + self.add_content_item("image_upload_input", upload_input) + + def _init_settings_tab_content(self) -> None: + """Инициализирует содержимое вкладки 'Настройки доступа'.""" + + self._init_access_rules_fields() + logger.debug("Settings tab content initialized") + + def _init_access_rules_fields(self) -> None: + """Инициализирует поля правил доступа.""" + + settings_container = self.page.locator(RackLocators.SETTINGS_CONTAINER) + + # Используем базовый метод для получения всех полей + fields_locators = self.get_input_fields_locators(settings_container) + + # Для каждого поля из маппинга проверяем наличие и инициализируем + for field_label, (_, input_name, list_name) in self.ACCESS_RULES_MAPPING.items(): + if field_label not in fields_locators: + continue + + self._init_single_access_rule_field( + field_label, fields_locators[field_label], input_name, list_name + ) + + logger.debug( + f"Settings tab content initialized. Found fields: {list(fields_locators.keys())}" + ) + + def _init_single_access_rule_field( + self, field_label: str, input_container: Any, input_name: str, list_name: str + ) -> None: + """Инициализирует одно поле правила доступа. + + Args: + field_label: Метка поля. + input_container: Контейнер поля ввода. + input_name: Имя поля ввода. + list_name: Имя списка. + """ + + try: + # Ищем input внутри контейнера + input_element = input_container.locator("input, textarea, select").first + if input_element.count() > 0: + field_input = TextInput(self.page, input_element, input_name) + self.add_content_item(input_name, field_input) + self.add_content_item(list_name, DropdownList(self.page)) + logger.debug(f"Initialized access rule field: '{field_label}'") + except Exception as e: + logger.error(f"Error initializing access rule field '{field_label}': {e}") + + def _init_toolbar_buttons(self) -> None: + """Инициализирует кнопки тулбара.""" + + self.add_button(self.page.locator(RackLocators.TOOLBAR_REPLACE_BUTTON), "replace") + self.add_button(self.page.locator(RackLocators.TOOLBAR_DONE_BUTTON), "save") + self.add_button(self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON), "cancel") + self.add_button(self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON), "delete") + + # Действия с вкладками + def switch_to_tab(self, tab_name: str) -> None: + """Переключается на указанную вкладку. + + Args: + tab_name: Название вкладки для переключения. + + Raises: + ValueError: Если указана неизвестная вкладка. + """ + + if tab_name not in self.tabs: + raise ValueError( + f"Unknown tab: {tab_name}. Available tabs: {list(self.tabs.keys())}" + ) + + self.tabs[tab_name].click() + self.wait_for_timeout(1000) + self.active_tab = tab_name + self._init_active_tab_content() + logger.info(f"Switched to tab: {tab_name}") + + def get_active_tab(self) -> str: + """Возвращает название активной вкладки. + + Returns: + Название активной вкладки. + """ + return self.active_tab + + def is_tab_active(self, tab_name: str) -> bool: + """Проверяет, активна ли указанная вкладка. + + Args: + tab_name: Название вкладки для проверки. + + Returns: + True если вкладка активна, иначе False. + """ + return self.active_tab == tab_name + + # Действия с изображениями + def upload_image(self, image_path: str) -> None: + """Загружает изображение на вкладке 'Изображение'. + + Args: + image_path: Путь к файлу изображения. + + Raises: + RuntimeError: Если не найден элемент для загрузки изображения. + """ + + if self.active_tab != self.TAB_IMAGE: + self.switch_to_tab(self.TAB_IMAGE) + + try: + self._perform_image_upload(image_path) + except Exception as e: + logger.error(f"Error uploading image: {e}") + raise + + def _perform_image_upload(self, image_path: str) -> None: + """Выполняет загрузку изображения. + + Args: + image_path: Путь к файлу изображения. + + Raises: + RuntimeError: Если не найден элемент для загрузки изображения. + """ + + upload_icon = self.get_content_item("image_upload_icon") + if upload_icon and upload_icon.count() > 0: + upload_icon.click() + self.wait_for_timeout(500) + + upload_input = self.get_content_item("image_upload_input") + if upload_input and upload_input.count() > 0: + upload_input.set_input_files(image_path) + logger.info(f"Uploaded image: {image_path}") + return + + # Пробуем найти input напрямую + file_input = self.page.locator(RackLocators.IMAGE_UPLOAD_INPUT) + if file_input.count() > 0: + file_input.first.set_input_files(image_path) + logger.info(f"Uploaded image via page input: {image_path}") + return + + raise RuntimeError("Image upload input not found") + + def has_current_image(self) -> bool: + """Проверяет, есть ли текущее изображение у стойки. + + Returns: + True если изображение загружено, иначе False. + """ + + if self.active_tab != self.TAB_IMAGE: + self.switch_to_tab(self.TAB_IMAGE) + + try: + return self._check_image_exists() + except Exception as e: + logger.error(f"Error checking for current image: {e}") + return False + + def _check_image_exists(self) -> bool: + """Проверяет наличие изображения. + + Returns: + True если изображение существует, иначе False. + """ + + image_container = self.page.locator(RackLocators.IMAGE_UPLOAD_CONTAINER) + img_element = image_container.locator(RackLocators.IMAGE_PREVIEW) + upload_icon = image_container.locator(RackLocators.IMAGE_UPLOAD_ICON) + + if img_element.count() > 0 and img_element.first.is_visible(): + return True + + return False + + # Действия с настройками доступа + def fill_access_rules( + self, + users_to_add: Optional[List[str]] = None, + target_fields: Optional[List[str]] = None + ) -> dict: + """Заполняет правила доступа в указанных полях. + + В каждый combobox добавляются указанные пользователи. + + Args: + users_to_add: Список пользователей для добавления. + target_fields: Список целевых полей для заполнения. + + Returns: + Словарь с результатами заполнения: + - access_rules_filled: количество добавленных пользователей + - errors: список ошибок + - fields_processed: обработанные поля + - field_stats: статистика по каждому полю + """ + + if self.active_tab != self.TAB_SETTINGS: + self.switch_to_tab(self.TAB_SETTINGS) + + results = self._init_fill_results() + + if users_to_add is None: + users_to_add = self._get_default_users() + + fields_to_process = self._get_fields_to_process(target_fields) + + logger.info(f"Processing fields: {[f[0] for f in fields_to_process]}") + logger.info(f"Users to add: {users_to_add}") + + for field_index, (field_label, _, input_name, _) in enumerate(fields_to_process): + self._process_single_field( + field_index, field_label, users_to_add, results + ) + + self._log_fill_summary(results, len(fields_to_process), len(users_to_add)) + + return results + + def _init_fill_results(self) -> dict: + """Инициализирует словарь результатов заполнения. + + Returns: + Словарь с начальными значениями результатов. + """ + + return { + "access_rules_filled": 0, + "errors": [], + "fields_processed": [], + "field_stats": {} + } + + def _get_default_users(self) -> List[str]: + """Возвращает список пользователей по умолчанию. + + Returns: + Список пользователей. + """ + + return [ + "TestUserRulesAdmin", + "TestUserRulesOper", + "TestUserRulesManager", + "TestUserRulesSec", + "TestUserRulesCollector" + ] + + def _process_single_field( + self, + field_index: int, + field_label: str, + users_to_add: List[str], + results: dict + ) -> None: + """Обрабатывает одно поле правил доступа. + + Args: + field_index: Индекс поля. + field_label: Название поля. + users_to_add: Список пользователей для добавления. + results: Словарь с результатами. + """ + + # Инициализируем статистику для поля + results["field_stats"][field_label] = { + "expected": len(users_to_add), + "added": 0, + "failed_users": [] + } + + field_locator = self.ACCESS_RULES_LOCATORS.get(field_label) + if not field_locator: + results["errors"].append(f"Locator not found for field '{field_label}'") + return + + try: + if field_index > 0: + self.page.mouse.click(10, 10) + self.wait_for_timeout(500) + + results["fields_processed"].append(field_label) + + # Находим элемент поля + field_element = self.page.locator(field_locator).first + if field_element.count() == 0: + results["errors"].append(f"Field element not found for '{field_label}'") + return + + self._clear_field(field_element) + + # Открываем combobox и добавляем пользователей + added_count, field_errors = self._open_dropdown_and_add_users( + field_element, field_label, users_to_add + ) + + results["access_rules_filled"] += added_count + results["field_stats"][field_label]["added"] = added_count + results["field_stats"][field_label]["failed_users"] = field_errors + results["errors"].extend([f"{field_label}: {error}" for error in field_errors]) + + # Закрываем dropdown + self.page.mouse.click(10, 10) + self.wait_for_timeout(500) + + logger.info(f"Field '{field_label}': added {added_count}/{len(users_to_add)} users") + + except Exception as e: + results["errors"].append(f"Error processing field {field_label}: {str(e)}") + + def _get_fields_to_process( + self, + target_fields: Optional[List[str]] = None + ) -> List[Tuple[str, str, str, str]]: + """Определяет поля для обработки. + + Args: + target_fields: Список целевых полей. + + Returns: + Список кортежей (field_label, attr_name, input_name, list_name). + """ + + if target_fields is None: + return list(self.ACCESS_RULES_MAPPING.items()) + + fields_to_process = [] + for field_attr in target_fields: + for field_label, (attr_name, input_name, list_name) in self.ACCESS_RULES_MAPPING.items(): + if attr_name == field_attr: + fields_to_process.append((field_label, attr_name, input_name, list_name)) + break + return fields_to_process + + def _clear_field(self, field_element: Any) -> None: + """Очищает поле от выбранных значений. + + Args: + field_element: Элемент поля для очистки. + """ + + parent_container = field_element.locator( + "xpath=ancestor::div[contains(@class, 'v-input')]" + ).first + + if parent_container.count() == 0: + return + + clear_button = parent_container.locator( + ".v-input__icon--clear button, .v-input__icon--append button, i.mdi-close-circle" + ).first + + if clear_button.count() > 0 and clear_button.is_visible(): + clear_button.click() + self.wait_for_timeout(500) + + def _open_dropdown_and_add_users( + self, + field_element: Any, + field_label: str, + users_to_add: List[str] + ) -> Tuple[int, List[str]]: + """Открывает выпадающий список и добавляет пользователей. + + Args: + field_element: Элемент поля. + field_label: Название поля. + users_to_add: Список пользователей для добавления. + + Returns: + Кортеж (количество добавленных, список ошибок). + """ + + added_count = 0 + errors = [] + + field_element.click(force=True) + self.wait_for_timeout(1500) + + dropdown_menu = self._get_dropdown_menu() + if not dropdown_menu: + errors.append(f"Could not open dropdown for {field_label}") + return added_count, errors + + for username in users_to_add: + added, error = self._add_user_to_dropdown(dropdown_menu, username, field_label) + if added: + added_count += 1 + if error: + errors.append(error) + + return added_count, errors + + def _get_dropdown_menu(self) -> Any: + """Возвращает выпадающее меню. + + Returns: + Locator выпадающего меню или None, если меню не найдено. + """ + + dropdown_menu = self.page.locator(RackLocators.MENU_ACTIVE_RACK_FORM).first + if dropdown_menu.count() == 0 or not dropdown_menu.is_visible(): + dropdown_menu = self.page.locator( + ".v-menu__content--active, .menuable__content__active" + ).first + + if dropdown_menu.count() == 0 or not dropdown_menu.is_visible(): + return None + return dropdown_menu + + def _add_user_to_dropdown( + self, + dropdown_menu: Any, + username: str, + field_label: str + ) -> Tuple[bool, Optional[str]]: + """Добавляет пользователя из выпадающего списка. + + Args: + dropdown_menu: Выпадающее меню. + username: Имя пользователя. + field_label: Название поля. + + Returns: + Кортеж (добавлен ли пользователь, сообщение об ошибке или None). + """ + + try: + user_item = dropdown_menu.locator(f"[role='listitem']:has-text('{username}')").first + if user_item.count() == 0: + user_item = dropdown_menu.locator(f"div:has-text('{username}')").first + + if user_item.count() > 0: + user_item.click() + self.wait_for_timeout(500) + return True, None + + return False, f"User '{username}' not found in dropdown for {field_label}" + except Exception as e: + return False, f"Failed to add user '{username}' to {field_label}: {str(e)}" + + def _log_fill_summary(self, results: dict, fields_count: int, users_count: int) -> None: + """Логирует итоговую статистику заполнения. + + Args: + results: Словарь с результатами. + fields_count: Количество полей. + users_count: Количество пользователей. + """ + + total_expected = fields_count * users_count + logger.info(f"Total added: {results['access_rules_filled']}/{total_expected}") + + for field_label, stats in results["field_stats"].items(): + if stats["added"] < stats["expected"]: + logger.warning( + f"Field '{field_label}' added only {stats['added']}/{stats['expected']} users. " + f"Failed: {stats['failed_users']}" + ) + + def verify_access_rules( + self, + expected_users: Optional[List[str]] = None, + target_fields: Optional[List[str]] = None + ) -> dict: + """Проверяет заполнение правил доступа в указанных полях. + + Проверяет, что в каждом поле есть все указанные пользователи. + + Args: + expected_users: Список ожидаемых пользователей. + target_fields: Список целевых полей для проверки. + + Returns: + Словарь с результатами проверки: + - total_expected_fields: общее количество ожидаемых значений + - correctly_filled: количество корректно заполненных + - incorrectly_filled: количество некорректно заполненных + - field_errors: список ошибок по полям + - fields_verified: список проверенных полей + - expected_users: список ожидаемых пользователей + """ + + if self.active_tab != self.TAB_SETTINGS: + self.switch_to_tab(self.TAB_SETTINGS) + + if expected_users is None: + expected_users = self._get_default_users() + + fields_to_verify = self._get_fields_to_process(target_fields) + total_expected_fields = len(fields_to_verify) * len(expected_users) + + results = { + "total_expected_fields": total_expected_fields, + "correctly_filled": 0, + "incorrectly_filled": 0, + "field_errors": [], + "fields_verified": [field_label for field_label, _, _, _ in fields_to_verify], + "expected_users": expected_users + } + + for field_label, _, _, _ in fields_to_verify: + self._verify_single_field(field_label, expected_users, results) + + if results["total_expected_fields"] > 0: + success_rate = results["correctly_filled"] / results["total_expected_fields"] * 100 + logger.info( + f"Access rules verification: {results['correctly_filled']}/" + f"{results['total_expected_fields']} ({success_rate:.1f}%)" + ) + + return results + + def _verify_single_field( + self, + field_label: str, + expected_users: List[str], + results: dict + ) -> None: + """Проверяет одно поле правил доступа. + + Args: + field_label: Название поля. + expected_users: Список ожидаемых пользователей. + results: Словарь с результатами для обновления. + """ + + field_locator = self.ACCESS_RULES_LOCATORS.get(field_label) + if not field_locator: + self._add_field_error( + results, field_label, expected_users, + f"Locator not found for field '{field_label}'" + ) + return + + try: + field_element = self.page.locator(field_locator).first + if field_element.count() == 0: + self._add_field_error( + results, field_label, expected_users, + f"Field '{field_label}' not found" + ) + return + + selected_users = self._get_selected_users(field_element) + logger.debug(f"Field '{field_label}' selected users: {selected_users}") + + for expected_user in expected_users: + if expected_user in selected_users: + results["correctly_filled"] += 1 + else: + results["incorrectly_filled"] += 1 + results["field_errors"].append( + f"Field '{field_label}' missing user: {expected_user} " + f"(selected: {selected_users})" + ) + + except Exception as e: + self._add_field_error( + results, field_label, expected_users, + f"Error verifying {field_label}: {str(e)}" + ) + logger.error(f"Error verifying {field_label}: {e}") + + def _get_selected_users(self, field_element: Any) -> List[str]: + """Получает список выбранных пользователей из поля. + + Args: + field_element: Элемент поля. + + Returns: + Список выбранных пользователей. + """ + + selected_users = [] + parent_container = field_element.locator( + "xpath=ancestor::div[contains(@class, 'v-input')]" + ).first + + if parent_container.count() == 0: + return selected_users + + selections_container = parent_container.locator( + ".v-select__selections, .v-chip__content" + ).first + + if selections_container.count() == 0: + return selected_users + + user_elements = selections_container.locator( + "span.v-chip__content, span.v-chip, span:not([class])" + ).all() + + for element in user_elements: + user_text = self._extract_user_text(element) + if user_text: + selected_users.append(user_text) + + return selected_users + + def _extract_user_text(self, element: Any) -> Optional[str]: + """Извлекает текст пользователя из элемента. + + Args: + element: Элемент DOM. + + Returns: + Текст пользователя или None. + """ + + user_text = element.text_content() or "" + user_text = user_text.strip() + + if not user_text or user_text in [",", " ", ""]: + return None + + if user_text.startswith(','): + user_text = user_text[1:].strip() + + return user_text if user_text else None + + def _add_field_error( + self, + results: dict, + field_label: str, + expected_users: List[str], + error_msg: str + ) -> None: + """Добавляет ошибку для поля. + + Args: + results: Словарь с результатами. + field_label: Название поля (не используется, оставлен для единообразия). + expected_users: Список ожидаемых пользователей. + error_msg: Сообщение об ошибке. + """ + + for _ in expected_users: + results["incorrectly_filled"] += 1 + results["field_errors"].append(error_msg) + + # Действия с кнопками + def click_close_button(self) -> None: + """Закрывает окно через кнопку 'Отменить'.""" + + self.page.mouse.click(10, 10) + self.wait_for_timeout(300) + + cancel_button = self.get_button_by_name("cancel") + if cancel_button: + cancel_button.click() + logger.debug("Clicked close button") + + def click_remove_button(self) -> None: + """Удаляет стойку с подтверждением.""" + + delete_button = self.get_button_by_name("delete") + if not delete_button: + return + + delete_button.click() + logger.debug("Clicked remove button") + self.wait_for_timeout(1500) + + self._confirm_deletion() + + def _confirm_deletion(self) -> None: + """Подтверждает удаление стойки.""" + + expected_title = "Удаление" + try: + self.delete_confirm.check_title( + title=expected_title, + msg=f"Expected title: '{expected_title}'" + ) + except AssertionError as e: + logger.warning(f"Dialog title mismatch: {e}") + + expected_message = f"Удалить {self.window_title}?" + try: + self.delete_confirm.check_text( + text=expected_message, + msg=f"Expected message: '{expected_message}'" + ) + except AssertionError as e: + logger.warning(f"Message text mismatch: {e}") + + self.delete_confirm.should_be_cancel_button() + self.delete_confirm.should_be_allow_button() + self.delete_confirm.click_allow_button() + self.wait_for_timeout(2000) + logger.debug("Remove confirmation completed") + + def click_done_button(self) -> None: + """Сохраняет изменения стойки.""" + + save_button = self.get_button_by_name("save") + if save_button: + save_button.click() + logger.debug("Clicked done button") + + def fill_rack_data(self, rack_data: RackEditData) -> dict: + """Заполняет поля формы редактирования стойки. + + Args: + rack_data: Данные для заполнения. + + Returns: + Словарь с результатами заполнения. + """ + + if self.active_tab != self.TAB_GENERAL: + self.switch_to_tab(self.TAB_GENERAL) + + results = { + "text_fields_filled": 0, + "combobox_fields_filled": 0, + "checkboxes_set": 0 + } + + self._fill_text_fields(rack_data, results) + self._fill_combobox_fields(rack_data, results) + self._set_checkbox(rack_data, results) + + return results + + def _fill_text_fields(self, rack_data: RackEditData, results: dict) -> 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 + ) -> 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.info(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: RackEditData, results: dict) -> 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 + ) -> None: + """Заполняет одно combobox поле. + + Args: + field_label: Метка поля. + input_name: Имя поля ввода. + list_name: Имя списка. + value: Значение для выбора. + results: Словарь с результатами. + """ + + try: + combobox_field = self.get_content_item(input_name) + if not combobox_field: + return + + combobox_field.click(force=True) + self.wait_for_timeout(500) + + dropdown_list = self.get_content_item(list_name) + if dropdown_list: + dropdown_list.click_item_with_text(value) + results["combobox_fields_filled"] += 1 + logger.info(f"Field '{field_label}' set: '{value}'") + except Exception as e: + logger.error(f"Error filling combobox '{field_label}': {e}") + + def _set_checkbox(self, rack_data: RackEditData, results: dict) -> None: + """Устанавливает чекбокс. + + Args: + rack_data: Данные для заполнения. + results: Словарь с результатами для обновления. + """ + + if rack_data.ventilation_panel is None: + return + + try: + checkbox = self.get_content_item("ventilation_checkbox") + if not checkbox: + return + + if rack_data.ventilation_panel: + checkbox.check(force=True) + else: + checkbox.uncheck(force=True) + + results["checkboxes_set"] += 1 + logger.info(f"Checkbox 'Ventilation panel' set to: {rack_data.ventilation_panel}") + except Exception as e: + logger.error(f"Error setting checkbox: {e}") + + # Проверки + def verify_all_filled_fields( + self, + rack_data: RackEditData, + skip_fields: Optional[List[str]] = None + ) -> dict: + """Проверяет, что все поля заполнены корректно. + + Args: + rack_data: Данные для проверки. + skip_fields: Список полей, которые нужно пропустить. + + Returns: + Словарь с результатами проверки. + """ + + if self.active_tab != self.TAB_GENERAL: + self.switch_to_tab(self.TAB_GENERAL) + + results = { + "total_expected_fields": 0, + "correctly_filled": 0, + "incorrectly_filled": 0, + "not_filled": 0, + "skipped_fields": 0, + "field_errors": [] + } + + if skip_fields is None: + skip_fields = [] + + self._verify_text_fields(rack_data, skip_fields, results) + self._verify_combobox_fields(rack_data, skip_fields, results) + self._verify_checkbox(rack_data, skip_fields, results) + + if results["total_expected_fields"] > 0: + success_rate = results["correctly_filled"] / results["total_expected_fields"] * 100 + logger.info( + f"Field check: {results['correctly_filled']}/" + f"{results['total_expected_fields']} ({success_rate:.1f}%)" + ) + + return results + + def _verify_text_fields( + self, + rack_data: RackEditData, + skip_fields: List[str], + results: dict + ) -> None: + """Проверяет текстовые поля. + + Args: + rack_data: Данные для проверки. + skip_fields: Список полей для пропуска. + results: Словарь с результатами для обновления. + """ + + for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items(): + expected_value = getattr(rack_data, attr_name, "") + if not expected_value or not str(expected_value).strip(): + continue + + results["total_expected_fields"] += 1 + + if field_label in skip_fields: + results["skipped_fields"] += 1 + continue + + self._verify_single_text_field(field_label, field_name, expected_value, results) + + def _verify_single_text_field( + self, + field_label: str, + field_name: str, + expected_value: str, + results: dict + ) -> None: + """Проверяет одно текстовое поле. + + Args: + field_label: Метка поля. + field_name: Имя поля. + expected_value: Ожидаемое значение. + results: Словарь с результатами. + """ + + try: + input_field = self.get_content_item(field_name) + if not input_field: + results["not_filled"] += 1 + results["field_errors"].append(f"Field '{field_label}' input not found") + return + + actual_value = input_field.get_input_value() + if actual_value == expected_value: + results["correctly_filled"] += 1 + else: + results["incorrectly_filled"] += 1 + results["field_errors"].append( + f"Field '{field_label}': expected '{expected_value}', got '{actual_value}'" + ) + except Exception as e: + results["not_filled"] += 1 + results["field_errors"].append(f"Error checking field '{field_label}': {str(e)}") + + def _verify_combobox_fields( + self, + rack_data: RackEditData, + skip_fields: List[str], + results: dict + ) -> None: + """Проверяет combobox поля. + + Args: + rack_data: Данные для проверки. + skip_fields: Список полей для пропуска. + results: Словарь с результатами для обновления. + """ + + for field_label, (attr_name, _, _) in self.COMBOBOX_FIELDS_MAPPING.items(): + expected_value = getattr(rack_data, attr_name, "") + if not expected_value or not str(expected_value).strip(): + continue + + results["total_expected_fields"] += 1 + + if field_label in skip_fields: + results["skipped_fields"] += 1 + continue + + self._verify_single_combobox_field(field_label, expected_value, results) + + def _verify_single_combobox_field( + self, + field_label: str, + expected_value: str, + results: dict + ) -> None: + """Проверяет одно combobox поле. + + Args: + field_label: Метка поля. + expected_value: Ожидаемое значение. + results: Словарь с результатами. + """ + + try: + actual_value = self._get_combobox_value(field_label) + actual_clean = actual_value.strip() if actual_value else "" + expected_clean = expected_value.strip() if expected_value else "" + + if actual_clean == expected_clean: + results["correctly_filled"] += 1 + else: + results["incorrectly_filled"] += 1 + results["field_errors"].append( + f"Combobox '{field_label}': expected '{expected_clean}', got '{actual_clean}'" + ) + except Exception as e: + results["not_filled"] += 1 + results["field_errors"].append(f"Error checking combobox '{field_label}': {e}") + + def _get_combobox_value(self, field_label: str) -> str: + """Получает значение из combobox поля. + + Args: + field_label: Название поля. + + Returns: + Значение поля или пустая строка. + """ + + actual_value = "" + locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label) + + if not locator: + return actual_value + + element = self.page.locator(locator).first + if element.count() == 0: + return actual_value + + selections_container = element.locator( + "xpath=ancestor::div[contains(@class, 'v-select__selections')]" + ).first + + if selections_container.count() > 0: + value_span = selections_container.locator("span").first + actual_value = value_span.text_content() or "" + + return actual_value + + def _verify_checkbox( + self, + rack_data: RackEditData, + skip_fields: List[str], + results: dict + ) -> None: + """Проверяет чекбокс. + + Args: + rack_data: Данные для проверки. + skip_fields: Список полей для пропуска. + results: Словарь с результатами для обновления. + """ + + if rack_data.ventilation_panel is None: + return + + results["total_expected_fields"] += 1 + + if "Вентиляционная панель" in skip_fields: + results["skipped_fields"] += 1 + return + + try: + checkbox = self.get_content_item("ventilation_checkbox") + if not checkbox: + results["not_filled"] += 1 + results["field_errors"].append("Checkbox 'Ventilation panel' not found") + return + + checkbox_state = checkbox.is_checked() + if checkbox_state == rack_data.ventilation_panel: + results["correctly_filled"] += 1 + else: + results["incorrectly_filled"] += 1 + expected_status = "enabled" if rack_data.ventilation_panel else "disabled" + actual_status = "enabled" if checkbox_state else "disabled" + results["field_errors"].append( + f"Checkbox 'Ventilation panel': expected '{expected_status}', " + f"got '{actual_status}'" + ) + except Exception as e: + results["not_filled"] += 1 + results["field_errors"].append(f"Error checking checkbox: {str(e)}") diff --git a/locators/rack_locators.py b/locators/rack_locators.py index 214af2f..b84159a 100644 --- a/locators/rack_locators.py +++ b/locators/rack_locators.py @@ -31,38 +31,36 @@ class RackLocators: # Контейнер формы создания/редактирования стойки FORM_INPUT_CONTAINER = "//div[contains(@class, 'flex xs6 pa-0')]" - # Локаторы полей формы создания стойки - RACK_NAME_FIELD = ("//div[contains(@class, 'container')]" - "//label[text()='Имя']/following-sibling::input") - RACK_HEIGHT_FIELD = ("//div[contains(@class, 'container')]" - "//div[contains(@class, 'v-input__slot') and " - ".//label[text()='Высота в юнитах']]") - RACK_DEPTH_FIELD = ("//div[contains(@class, 'container')]" - "//div[contains(@class, 'v-input__slot') and " - ".//label[text()='Глубина (мм)']]") - RACK_SERIAL_FIELD = ("//div[contains(@class, 'container')]" - "//label[text()='Серийный номер']/following-sibling::input") - RACK_INVENTORY_FIELD = ("//div[contains(@class, 'container')]" - "//label[text()='Инвентарный номер']/following-sibling::input") - RACK_COMMENT_FIELD = ("//div[contains(@class, 'container')]" - "//label[text()='Комментарий']/following-sibling::input") - RACK_CABLE_ENTRY_FIELD = ("//div[contains(@class, 'container')]" - "//div[contains(@class, 'v-input__slot') and " - ".//label[text()='Ввод кабеля']]") - RACK_STATE_FIELD = ("//div[contains(@class, 'container')]" - "//div[contains(@class, 'v-input__slot') and " - ".//label[text()='Состояние']]") - RACK_OWNER_FIELD = ("//div[contains(@class, 'container')]" - "//div[contains(@class, 'v-input__slot') and " - ".//label[text()='Владелец']]") - RACK_SERVICE_ORG_FIELD = ("//div[contains(@class, 'container')]" - "//div[contains(@class, 'v-input__slot') and " - ".//label[text()='Обслуживающая организация']]") - RACK_PROJECT_FIELD = ("//div[contains(@class, 'container')]" - "//div[contains(@class, 'v-input__slot') and " - ".//label[text()='Проект/Титул']]") + # Форма редактирования стойки в модальном окне + RACK_EDIT_FORM = "[data-testid='cabinet-bar__cabinet-form']" - # Локаторы для выпадающего меню + # Локаторы полей формы + INPUT_FORM_RACK_DATA = f"{RACK_EDIT_FORM}" + INPUT_FORM_RACK_DATA_FIELD_NAME = "[data-testid='cabinet-bar__main__text-field__name']" + INPUT_FORM_RACK_DATA_FIELD_COMMENT = "[data-testid='cabinet-bar__main__text-field__comment']" + INPUT_FORM_RACK_DATA_FIELD_SERIAL = "[data-testid='cabinet-bar__main__text-field__serial_number']" + INPUT_FORM_RACK_DATA_FIELD_INVENTORY = "[data-testid='cabinet-bar__main__text-field__inventory_number']" + INPUT_FORM_RACK_DATA_FIELD_POWER = "[data-testid='cabinet-bar__main__text-field__allocated_power']" + + # Локаторы для combobox полей + INPUT_FORM_RACK_DATA_FIELD_CABLE_ENTRY = "[data-testid='cabinet-bar__select_enum__select-field__cable_input']" + INPUT_FORM_RACK_DATA_FIELD_CONDITION_TYPE = "[data-testid='cabinet-bar__select_enum__select-field__condition_type']" + INPUT_FORM_RACK_DATA_FIELD_DEPTH = "[data-testid='cabinet-bar__select_enum__select-field__depth']" + INPUT_FORM_RACK_DATA_FIELD_USIZE = "[data-testid='cabinet-bar__select_enum__select-field__usize']" + INPUT_FORM_RACK_DATA_FIELD_OWNER = "[data-testid='cabinet-bar__select__select-field__owner']" + INPUT_FORM_RACK_DATA_FIELD_SERVICE_PROVIDER = "[data-testid='cabinet-bar__select__select-field__service_provider']" + INPUT_FORM_RACK_DATA_FIELD_PROJECT = "[data-testid='cabinet-bar__select__select-field__project']" + + # Чекбоксы + INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel'] input[type='checkbox']" + INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_LABEL = "label:has-text('Вентиляционная панель')" + INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_CONTAINER = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel']" + + # Локаторы для меню combobox + MENU_ACTIVE_RACK_FORM = "//div[contains(@class, 'menuable__content__active')]" + MENU_ACTIVE_ITEMS = "//div[@role='list']//div[@role='listitem']" + + # Локаторы для выпадающего меню (которые использовались в старом коде) DROPDOWN_LIST = 'div.menuable__content__active div[role="list"]' DROPDOWN_ITEM_BY_TEXT = ('div.menuable__content__active ' 'div[role="listitem"]:has(span:has-text("{}"))') @@ -126,3 +124,34 @@ class RackLocators: # Кнопки подтверждения удаления CONFIRM_REMOVE_YES_BUTTON = "[data-testid='cabinet-bar__card_confirmation__btn__yes']" CONFIRM_REMOVE_NO_BUTTON = "[data-testid='cabinet-bar__card_confirmation__btn__no']" + + # ================ ЛОКАТОРЫ ДЛЯ ВКЛАДОК в модальном окне редактирования == + + # Локаторы для вкладок в модальном окне редактирования + MODAL_TAB_GENERAL = "[data-testid='cabinet-bar__main_tab']" + MODAL_TAB_IMAGE = "[data-testid='cabinet-bar__photo_tab']" + MODAL_TAB_SETTINGS = "[data-testid='cabinet-bar__settings_tab']" + + # ================ ЛОКАТОРЫ ДЛЯ ВКЛАДКИ "Изображение" =================== + + IMAGE_UPLOAD_CONTAINER = "div.layout.column.fill-height.justify-center.align-center" + IMAGE_UPLOAD_ICON = "i.mdi-add_photo_alternate" + IMAGE_UPLOAD_INPUT = "input.button-file-upload__input[type='file']" + IMAGE_PREVIEW = "img" + IMAGE_CONTAINER = "div.layout.column.fill-height.justify-center.align-center" + + # ================ ЛОКАТОРЫ ДЛЯ ВКЛАДКИ "НАСТРОЙКИ" =================== + + # Контейнер вкладки "Настройки" + SETTINGS_CONTAINER = "div.layout.back.fill-height.justify-start" + SETTINGS_ACCESS_MANAGER_TITLE = "div.v-toolbar__title:has-text('Менеджер доступа')" + + # Локаторы для полей правил доступа + SETTINGS_READ_RULES = "[data-testid='LOCATION_SETTINGS__select__rules.read']" + SETTINGS_WRITE_RULES = "[data-testid='LOCATION_SETTINGS__select__rules.write']" + SETTINGS_SMS_RULES = "[data-testid='LOCATION_SETTINGS__select__rules.sms']" + SETTINGS_EMAIL_RULES = "[data-testid='LOCATION_SETTINGS__select__rules.email']" + SETTINGS_PUSH_RULES = "[data-testid*='rules.push']" + + # Кнопки вкладки "Настройки" + SETTINGS_CANCEL_BUTTON = "[data-testid='LOCATION_SETTINGS__btn__cancel']" \ No newline at end of file diff --git a/tests/e2e/elements/test_edit_rack.py b/tests/e2e/elements/test_edit_rack.py new file mode 100644 index 0000000..45a3e5e --- /dev/null +++ b/tests/e2e/elements/test_edit_rack.py @@ -0,0 +1,414 @@ +"""Модуль тестов вкладки 'Стойка' в модуле Объекты. + +Содержит тесты для проверки функциональности +работы со стойкой оборудования. +""" + +import os +import pytest +from playwright.sync_api import Page +from locators.navigation_panel_locators import NavigationPanelLocators +from pages.login_page import LoginPage +from pages.main_page import MainPage +from pages.location_page import LocationPage +from pages.rack_page import RackPage +from components_derived.accounting_objects.rack_maker import RackObjectMaker, RackData +from components_derived.frames.create_child_element_frame import CreateChildElementFrame +from components_derived.modal_edit_rack import ModalEditRack, RackEditData +from tools.logger import get_logger + +# Константы +RACK_NAME = "Test-Rack-Functionality" + +# Инициализация логгера для всего модуля +logger = get_logger("RACK_TESTS") +logger.setLevel("INFO") + +class TestRackTab: + """Набор тестов для вкладки 'Стойка' в модуле Объекты. + + Проверяет корректность отображения, функциональность элементов интерфейса + и переключение между вкладками стойки оборудования. + + Тесты покрывают следующие функциональные области: + 1. test_rack_general_info_tab_fields - Заполнение полей вкладки 'Общая информация' + 2. test_rack_image_tab - Работа с вкладкой 'Изображение' + 3. test_rack_access_rules - Заполнение полей правил доступа + """ + + # Инициализируем атрибуты + main_page: MainPage = None + location_page: LocationPage = None + + def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: + """Проверяет существование стойки. + + Args: + browser: Страница Playwright + rack_name: Имя стойки для проверки + + Returns: + bool: True если стойка существует, False в противном случае + """ + + # Обновляем навигационную панель + self.main_page.wait_for_timeout(500) + self.main_page.click_subpanel_item("test-zone") + + nav_panel_locator = NavigationPanelLocators.TREEVIEW + + # Проверяем видимость элемента + element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first + + if element.is_visible(): + return True + return False + + def _create_rack(self, browser: Page, rack_name: str) -> None: + """Создает стойку. + + Args: + browser: Страница Playwright + rack_name: Имя стойки для создания + """ + logger.debug(f"Creating rack: {rack_name}") + + # Нажимаем кнопку "Создать" на тулбаре + self.location_page.click_create_button() + + # Создаем фрейм создания дочернего элемента + create_child_frame = CreateChildElementFrame(browser) + + # Нажимаем на плашку "Класс объекта учета" + create_child_frame.open_object_class_combobox() + + # Из выпадающего меню выбираем пункт "Стойка" + create_child_frame.select_object_class("Стойка") + + # Открывается набор плашек для задания параметров стойки + rack_maker = RackObjectMaker(browser) + + # Создаем объект данных стойки + rack_data = RackData( + name=rack_name, + height="42", + depth="1000" + ) + + # Заполняем данные стойки + rack_maker.fill_rack_data(rack_data) + + # Нажимаем кнопку создания + create_child_frame.click_add_button() + + logger.info(f"Rack '{rack_name}' created successfully") + + def _delete_rack_from_context_menu(self, browser: Page, rack_name: str) -> None: + """Удаляет стойку через контекстное меню. + + Args: + browser: Страница Playwright + rack_name: Имя стойки для удаления + """ + + # 1. Находим элемент стойки в навигационной панели + rack_element = browser.locator( + NavigationPanelLocators.TREEVIEW + ).get_by_text(rack_name, exact=True).first + + # Прокручиваем до элемента если нужно + rack_element.scroll_into_view_if_needed() + self.main_page.wait_for_timeout(500) + + # 2. Проверяем и нажимаем кнопку "Изменить" + rack_page = RackPage(browser) + + # Проверяем видимость кнопки + rack_page.toolbar.check_button_visibility("edit") + + # Проверяем тултип кнопки + rack_page.toolbar.check_button_tooltip("edit", "Изменить") + + # Кликаем на кнопку "Изменить" + rack_page.toolbar.get_button_by_name("edit").click() + + # 3. Создаем экземпляр ModalRackEditRack + rack_edit = ModalEditRack(browser, rack_name) + + # Используем метод для удаления + rack_edit.click_remove_button() + + logger.info(f"Rack '{rack_name}' deleted successfully") + + @pytest.fixture(scope="function", autouse=True) + def setup(self, browser: Page) -> None: + """Фикстура для подготовки тестового окружения. + + Выполняет: + 1. Авторизацию в системе + 2. Создание стойки если она не существует + 3. Переход к стойке + + Args: + browser (Page): Экземпляр страницы Playwright для взаимодействия с UI + """ + + # Авторизация в системе + login_page = LoginPage(browser) + login_page.do_login() + + # Мы на главной странице + self.main_page = MainPage(browser) + self.main_page.should_be_navigation_panel() + + # Переходим к Объектам + self.main_page.click_main_navigation_panel_item("Объекты") + self.main_page.wait_for_timeout(1000) + self.main_page.click_main_navigation_panel_item("test-zone") + + # Создаем экземпляр страницы локации + self.location_page = LocationPage(browser) + + # Проверяем существование стойки + if not self._check_rack_existance(browser, RACK_NAME): + self._create_rack(browser, RACK_NAME) + self.main_page.wait_for_timeout(3000) + else: + logger.info(f"Rack '{RACK_NAME}' already exists") + + # Переходим к стойке для тестирования + self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") + self.main_page.wait_for_timeout(3000) + + @pytest.fixture(scope="class", autouse=True) + def cleanup_rack(self, browser: Page): + """Фикстура для очистки созданной стойки после ВСЕХ тестов класса. + + Выполняется один раз после завершения всех тестов класса TestRackTab. + Удаляет созданную стойку. + + Args: + browser: Экземпляр страницы Playwright + """ + + # Тесты выполняются здесь + yield + + # Переходим на главную страницу и в нужную зону + login_page = LoginPage(browser) + login_page.do_login() + + self.main_page = MainPage(browser) + self.main_page.should_be_navigation_panel() + + # Переходим к Объектам + self.main_page.click_main_navigation_panel_item("Объекты") + self.main_page.wait_for_timeout(1000) + self.main_page.click_main_navigation_panel_item("test-zone") + self.main_page.wait_for_timeout(1000) + + # Проверяем существование стойки + if self._check_rack_existance(browser, RACK_NAME): + + # Переходим на страницу стойки + self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") + self.main_page.wait_for_timeout(2000) + + # Удаляем стойку + self._delete_rack_from_context_menu(browser, RACK_NAME) + + # Дополнительная проверка + self.main_page.click_subpanel_item("test-zone") + self.main_page.wait_for_timeout(1000) + + #@pytest.mark.develop + def test_rack_general_info_tab_fields(self, browser: Page) -> None: + """Тест заполнения полей вкладки 'Общая информация' стойки.""" + + rack_page = RackPage(browser) + + # Переходим в режим редактирования + rack_page.click_edit_button() + rack_page.wait_for_timeout(1000) + + # Создаем экземпляр ModalEditRack + rack_edit = ModalEditRack(browser, RACK_NAME) + + # Создаем тестовые данные для заполнения всех полей + rack_edit_data = RackEditData( + # Основные поля + name="Test-Rack-Functionality", + serial="SN123456789", + inventory="INV987654321", + comment="Тестовый комментарий для стойки (обновленный)", + allocated_power="5500", + + # Combobox поля + cable_entry="сверху", + state="Введен в эксплуатацию", + owner="", + service_org="", + project="", + + # Checkbox поля + ventilation_panel=True, + ) + + # Заполняем все поля формы + results = rack_edit.fill_rack_data(rack_edit_data) + + logger.debug(f"Fill results: {results}") + + # Сохраняем изменения + rack_edit.click_done_button() + rack_edit.wait_for_timeout(3000) + + # Вход в режим редактирования + rack_page.click_edit_button() + + # Проверяем поля, пропуская недоступные + verification_results = rack_edit.verify_all_filled_fields( + rack_edit_data, + skip_fields=["Владелец", "Обслуживающая организация", "Проект/Титул"] + ) + logger.debug(f"Verification results: {verification_results}") + + # Проверяем результаты + assert verification_results["incorrectly_filled"] == 0, \ + f"Available fields incorrectly filled: {verification_results['field_errors']}" + assert verification_results["not_filled"] == 0, \ + f"Available fields not filled: {verification_results['field_errors']}" + + rack_edit.click_close_button() + + #@pytest.mark.develop + def test_rack_image_tab(self, browser: Page) -> None: + """Тест вкладки 'Изображение' стойки.""" + + rack_page = RackPage(browser) + + # Переходим в режим редактирования + rack_page.click_edit_button() + rack_page.wait_for_timeout(1000) + + # Создаем экземпляр ModalEditRack + rack_edit = ModalEditRack(browser, RACK_NAME) + + # Переключаемся на вкладку "Изображение" + rack_edit.switch_to_tab(ModalEditRack.TAB_IMAGE) + + # Проверяем вкладку + assert rack_edit.is_tab_active(ModalEditRack.TAB_IMAGE), \ + "Image tab should be active" + + # Загружаем изображение если есть + test_image_path = os.path.join(os.path.dirname(__file__), "test_image.jpg") + if os.path.exists(test_image_path): + logger.debug(f"Found test image: {test_image_path}") + # Находим input и загружаем файл + file_input = browser.locator("input[type='file']") + if file_input.count() == 0: + file_input = browser.locator(".button-file-upload__input") + + if file_input.count() > 0: + file_input.set_input_files(test_image_path) + rack_page.wait_for_timeout(2000) + logger.debug("Test image uploaded") + else: + logger.warning(f"Test image not found at: {test_image_path}") + + # Сохраняем + rack_edit.click_done_button() + + @pytest.mark.develop + def test_rack_access_rules(self, browser: Page) -> None: + """Тест заполнения полей правил доступа. + + В каждое поле добавляются ВСЕ пользователи из списка custom_users. + """ + + rack_page = RackPage(browser) + + # Переходим в режим редактирования + rack_page.click_edit_button() + rack_page.wait_for_timeout(1000) + + # Создаем экземпляр ModalEditRack + rack_edit = ModalEditRack(browser, RACK_NAME) + + # Переключаемся на вкладку "Настройки" + rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS) + + # Проверяем, что вкладка активна + assert rack_edit.is_tab_active(ModalEditRack.TAB_SETTINGS), \ + "Settings tab should be active after switching" + + # Целевые поля для заполнения + target_fields = [ + "read_access_rules", + "write_access_rules", + "sms_access_rules", + "email_access_rules", + "push_access_rules" + ] + + # Пользователи для добавления в каждое поле + custom_users = [ + "TestUserRulesAdmin", + "TestUserRulesOper", + "TestUserRulesManager", + "TestUserRulesSec", + "TestUserRulesCollector" + ] + + # Заполняем поля + fill_results = rack_edit.fill_access_rules( + users_to_add=custom_users, + target_fields=target_fields + ) + + # Проверяем, что все пользователи были добавлены + expected_total = len(target_fields) * len(custom_users) + assert fill_results["access_rules_filled"] == expected_total, \ + f"Added {fill_results['access_rules_filled']} users, expected {expected_total}" + assert len(fill_results["errors"]) == 0, \ + f"Errors during filling: {fill_results['errors']}" + + # Проверяем заполнение + verification_results = rack_edit.verify_access_rules( + expected_users=custom_users, + target_fields=target_fields + ) + logger.debug(f"Verification results: {verification_results}") + + # Проверяем результаты + assert verification_results["correctly_filled"] == expected_total, \ + f"Correctly filled {verification_results['correctly_filled']} out of {expected_total}" + assert verification_results["incorrectly_filled"] == 0, \ + f"Verification errors: {verification_results['field_errors']}" + + # Дополнительная проверка + assert len(verification_results["fields_verified"]) == len(target_fields), \ + f"Fields verified: {len(verification_results['fields_verified'])}, expected: {len(target_fields)}" + + # Сохраняем изменения + rack_edit.click_done_button() + rack_edit.wait_for_timeout(3000) + + # Возвращаемся в режим редактирования и проверяем снова + rack_page.click_edit_button() + rack_page.wait_for_timeout(1000) + + rack_edit = ModalEditRack(browser, RACK_NAME) + rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS) + + verification_results_after_save = rack_edit.verify_access_rules( + expected_users=custom_users, + target_fields=target_fields + ) + logger.debug(f"Verification results after save: {verification_results_after_save}") + + assert verification_results_after_save["correctly_filled"] == expected_total, \ + f"After save - correctly filled {verification_results_after_save['correctly_filled']} out of {expected_total}" + + rack_edit.click_close_button() diff --git a/tests/e2e/elements/test_image.jpg b/tests/e2e/elements/test_image.jpg new file mode 100644 index 0000000..1e21bdc Binary files /dev/null and b/tests/e2e/elements/test_image.jpg differ