From 852b42f22613d0d0b7db389c3f7bc508e3473d68 Mon Sep 17 00:00:00 2001 From: Radislav Date: Fri, 20 Feb 2026 11:29:09 +0300 Subject: [PATCH] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/base_component.py | 10 +- components/dropdown_list_component.py | 19 +- components/dynamic_form_component.py | 151 ++ components/form_field_component.py | 644 ++++++++ components/modal_window_component.py | 5 + components/navbar_component_.py | 351 +++++ .../__pycache__/rack_maker.cpython-313.pyc | Bin 14467 -> 0 bytes ...create_child_element_frame.cpython-313.pyc | Bin 16678 -> 0 bytes components_derived/modal_edit_rack.py | 1401 +++++++++++++++++ locators/modal_window_locators.py | 13 +- locators/rack_locators.py | 33 + pages/main_page.py | 39 + pages/ztp_templates_tab.py | 68 +- tests/e2e/elements/test_edit_rack.py | 414 +++++ tests/e2e/elements/test_element_rack.py | 233 ++- tests/e2e/elements/test_image.jpg | Bin 0 -> 5340 bytes tests/e2e/ztp/test_expand_navigation_panel.py | 168 ++ tests/e2e/ztp/test_ztp_templates_tab.py | 90 +- 18 files changed, 3482 insertions(+), 157 deletions(-) create mode 100644 components/dynamic_form_component.py create mode 100644 components/form_field_component.py create mode 100644 components/navbar_component_.py delete mode 100644 components_derived/accounting_objects/__pycache__/rack_maker.cpython-313.pyc delete mode 100644 components_derived/frames/__pycache__/create_child_element_frame.cpython-313.pyc create mode 100644 components_derived/modal_edit_rack.py create mode 100644 tests/e2e/elements/test_edit_rack.py create mode 100644 tests/e2e/elements/test_image.jpg create mode 100644 tests/e2e/ztp/test_expand_navigation_panel.py diff --git a/components/base_component.py b/components/base_component.py index 83f92a9..778144c 100644 --- a/components/base_component.py +++ b/components/base_component.py @@ -51,14 +51,14 @@ class BaseComponent: fields_locators = {} - layouts = container_locator.locator("div.layout") + layouts = container_locator.locator("div.layout > div.flex").locator("..") for i in range(layouts.count()): layout = layouts.nth(i) flex_containers = layout.locator("div.flex") # Обрабатываем пары контейнеров - for j in range(0, flex_containers.count() - 1): + for j in range(0, flex_containers.count() - 1, 2): label_container = flex_containers.nth(j) input_container = flex_containers.nth(j + 1) @@ -71,9 +71,11 @@ class BaseComponent: has_input = input_container.locator( "input, textarea, select" ).count() > 0 + + not_found = fields_locators.get(label_text) is None - if has_input: - fields_locators[label_text] = input_container + if has_input and not_found: + fields_locators[label_text] = input_container return fields_locators diff --git a/components/dropdown_list_component.py b/components/dropdown_list_component.py index 9df5267..08606b7 100644 --- a/components/dropdown_list_component.py +++ b/components/dropdown_list_component.py @@ -214,15 +214,30 @@ class DropdownList(BaseComponent): Raises: AssertionError: Если элемент отсутствует или недоступен. """ + # Получаем текущий открытый dropdown menu + dropdown_menu = self.page.locator(".v-menu__content--active, .menuable__content__active").first + if dropdown_menu.count() > 0 and dropdown_menu.is_visible(): + # Ищем span с точным текстом + element = dropdown_menu.locator(f"span:has-text('{text}')").first + if element.count() > 0: + logger.debug(f"Found user '{text}' directly with span selector") + if element.is_enabled(): + return + else: + # Проверяем родительский a + parent_a = element.locator("xpath=ancestor::a").first + if parent_a.count() > 0 and parent_a.is_enabled(): + return + + # Fallback на старый метод element = self.page.get_by_role("listitem").filter(has_text=text) if element.count() > 1: rtext = f"^{text}$" element = self.page.get_by_role("listitem").filter( has_text=re.compile(rtext) ) - enabled = element.is_enabled() - if not enabled: + if not element.first.is_enabled(): assert False, f"Dropdown list item '{text}' is missing or disabled" def check_vertical_scrolling(self, locator: str | Locator) -> bool: diff --git a/components/dynamic_form_component.py b/components/dynamic_form_component.py new file mode 100644 index 0000000..fa99fce --- /dev/null +++ b/components/dynamic_form_component.py @@ -0,0 +1,151 @@ +# components/dynamic_form_component.py +"""Универсальный компонент для работы с динамическими формами.""" + +from typing import Optional, Dict, List, Any +from playwright.sync_api import Page, Locator +from tools.logger import get_logger + +logger = get_logger("DYNAMIC_FORM_COMPONENT") + + +class DynamicFormComponent: + """Компонент для работы с формами, находит поля по меткам.""" + + def __init__(self, page: Page, form_selector: str = "form, .v-form"): + self.page = page + self.form_selector = form_selector + self._form_container = None + self._field_labels_cache = {} + + def _get_form_container(self) -> Locator: + """Получает контейнер формы.""" + if self._form_container is None: + container = self.page.locator(self.form_selector) + try: + container.wait_for(state="visible", timeout=5000) + self._form_container = container + except: + raise ValueError(f"Form container not found: {self.form_selector}") + return self._form_container + + def get_all_field_labels(self) -> List[str]: + """Получает все метки полей в форме.""" + self._load_field_labels() + return list(self._field_labels_cache.keys()) + + def _load_field_labels(self) -> None: + """Загружает метки полей формы.""" + if self._field_labels_cache: + return + + form = self._get_form_container() + labels = {} + + # Ищем все элементы с текстом, которые могут быть метками + # Адаптируйте селекторы под вашу структуру DOM + label_elements = form.locator( + ".v-label, label, .field-label, [class*='label']" + ) + + for i in range(label_elements.count()): + elem = label_elements.nth(i) + label_text = elem.text_content().strip() + if label_text and len(label_text) < 100: # Исключаем большие тексты + labels[label_text] = elem + + self._field_labels_cache = labels + + def get_field_by_label(self, label_text: str) -> Optional[Locator]: + """Находит поле по метке.""" + self._load_field_labels() + + # Прямое совпадение + if label_text in self._field_labels_cache: + return self._get_field_input(self._field_labels_cache[label_text]) + + # Частичное совпадение + for label, element in self._field_labels_cache.items(): + if label_text in label or label in label_text: + return self._get_field_input(element) + + logger.warning(f"Поле с меткой '{label_text}' не найдено") + return None + + def _get_field_input(self, label_element: Locator) -> Optional[Locator]: + """Получает элемент ввода рядом с меткой.""" + # Разные стратегии поиска input элемента + strategies = [ + lambda: label_element.locator("+ input, + textarea").first, + lambda: label_element.locator("../..").locator("input, textarea").first, + lambda: self.page.locator(f"input[aria-label*='{label_element.text_content()}']"), + lambda: self.page.locator(f"input[placeholder*='{label_element.text_content()}']"), + ] + + for strategy in strategies: + try: + input_elem = strategy() + if input_elem.count() > 0: + return input_elem + except: + continue + + return None + + def get_field_type_by_label(self, label_text: str) -> str: + """Определяет тип поля по метке.""" + field_element = self.get_field_by_label(label_text) + if not field_element: + return "unknown" + + # Определяем тип по атрибутам + input_type = field_element.get_attribute("type") + role = field_element.get_attribute("role") + + if input_type == "checkbox" or role == "checkbox": + return "checkbox" + elif role == "combobox" or field_element.get_attribute("aria-haspopup") == "listbox": + return "combobox" + else: + return "text" + + def fill_field_by_label(self, label_text: str, value: Any) -> bool: + """Заполняет поле по метке.""" + field_type = self.get_field_type_by_label(label_text) + + if field_type == "text": + return self._fill_text_field_by_label(label_text, str(value)) + elif field_type == "combobox": + return self._fill_combobox_by_label(label_text, str(value)) + elif field_type == "checkbox": + return self._set_checkbox_by_label(label_text, bool(value)) + else: + logger.warning(f"Неизвестный тип поля для '{label_text}'") + return False + + def _fill_text_field_by_label(self, label_text: str, value: str) -> bool: + """Заполняет текстовое поле по метке.""" + field = self.get_field_by_label(label_text) + if not field: + return False + + try: + field.click() + field.fill("") + field.fill(value) + + # Проверяем результат + actual_value = field.input_value() + if actual_value == value: + logger.debug(f"✓ Заполнено поле '{label_text}': '{value}'") + return True + else: + logger.warning(f"Несоответствие значения для '{label_text}'") + return False + except Exception as e: + logger.error(f"Ошибка при заполнении поля '{label_text}': {e}") + return False + + def _fill_combobox_by_label(self, label_text: str, value: str) -> bool: + """Заполняет combobox по метке.""" + # Реализация аналогичная modal_rack_edit.py + # ... diff --git a/components/form_field_component.py b/components/form_field_component.py new file mode 100644 index 0000000..43c2805 --- /dev/null +++ b/components/form_field_component.py @@ -0,0 +1,644 @@ +"""Модуль для работы с полями формы.""" + +import re +from typing import Dict, Optional, List, Callable +from playwright.sync_api import Page, Locator +from tools.logger import get_logger + +logger = get_logger("FORM_FIELD_COMPONENT") + + +class FormFieldComponent: + """Компонент для работы с полями формы.""" + + def __init__(self, page: Page): + """ + Инициализирует компонент для работы с полями формы. + + Args: + page (Page): Экземпляр страницы Playwright + """ + self.page = page + self._form_fields = None + + # Колбэк для загрузки полей + self._load_form_fields_callback = None + + def set_form_fields(self, form_fields: Dict[str, Locator]) -> None: + """ + Устанавливает поля формы для работы. + + Args: + form_fields: Словарь с полями формы {название: локатор} + """ + self._form_fields = form_fields + logger.debug(f"Set {len(form_fields)} form fields") + + def set_load_form_fields_callback(self, callback: Callable[[], Dict[str, Locator]]) -> None: + """ + Устанавливает колбэк для ленивой загрузки полей формы. + + Args: + callback: Функция, которая возвращает словарь полей формы + """ + self._load_form_fields_callback = callback + + def get_available_fields(self) -> list: + """ + Получает список доступных полей. + Если поля не загружены и есть колбэк для загрузки - загружает их. + + Returns: + list: Список названий полей + """ + if not self._form_fields: + if self._load_form_fields_callback: + logger.debug("Lazy loading form fields via callback...") + form_fields = self._load_form_fields_callback() + self.set_form_fields(form_fields) + else: + logger.warning("No form fields set and no load callback available") + return [] + + return list(self._form_fields.keys()) + + def get_combobox_options(self, field_name: str) -> List[str]: + """ + Получает список доступных опций из combobox поля. + + Args: + field_name: Название combobox поля + + Returns: + list[str]: Список доступных опций + """ + if not self._form_fields: + logger.warning("No form fields set") + return [] + + if field_name not in self._form_fields: + logger.debug(f"Combobox field '{field_name}' not found") + return [] + + field_container = self._form_fields[field_name] + + try: + # Открываем combobox + if not self._open_combobox(field_container): + return [] + + # Получаем опции из открытого меню + options = self._get_dropdown_options() + + # Закрываем combobox + self.page.keyboard.press("Escape") + self.page.wait_for_timeout(500) + + logger.debug(f"Found {len(options)} options for '{field_name}': {options}") + return options + + except Exception as e: + logger.error(f"Error getting combobox options for '{field_name}': {e}") + self.page.keyboard.press("Escape") + return [] + + def get_selected_combobox_value(self, field_name: str) -> str: + """ + Получает выбранное значение из combobox. + + Args: + field_name: Название combobox поля + + Returns: + str: Выбранное значение или пустая строка если ничего не выбрано + """ + if not self._form_fields: + logger.warning("No form fields set") + return "" + + if field_name not in self._form_fields: + logger.debug(f"Combobox field '{field_name}' not found") + return "" + + field_container = self._form_fields[field_name] + + try: + # Ищем span элементы с текстом + span_locator = field_container.locator("span") + if span_locator.count() == 0: + # Пробуем найти в input + input_locator = field_container.locator("input") + if input_locator.count() > 0: + return input_locator.first.input_value().strip() + return "" + + # Ищем непустой текст в span + for i in range(span_locator.count()): + span_text = span_locator.nth(i).text_content().strip() + if span_text: + # Пропускаем заголовочные или системные тексты + if any(skip_text in span_text.lower() for skip_text in ['выберите', 'select', 'не выбрано']): + continue + logger.debug(f"Selected value for '{field_name}': '{span_text}'") + return span_text + + return "" + + except Exception as e: + logger.error(f"Error getting selected value for '{field_name}': {e}") + return "" + + def get_field_value(self, field_name: str) -> str: + """ + Получает значение поля (универсальный метод для разных типов полей). + + Args: + field_name: Название поля + + Returns: + str: Значение поля или пустая строка если поле не найдено + """ + if not self._form_fields or field_name not in self._form_fields: + return "" + + field_container = self._form_fields[field_name] + + try: + # Пробуем получить значение input + input_field = field_container.locator("input, textarea").first + if input_field.count() > 0: + return input_field.input_value().strip() + + # Для combobox получаем выбранное значение + combobox_value = self.get_selected_combobox_value(field_name) + if combobox_value: + return combobox_value + + # Для чекбокса получаем состояние + checkbox_state = self.is_checkbox_checked(field_name) + if checkbox_state is not None: + return "checked" if checkbox_state else "unchecked" + + return "" + + except Exception as e: + logger.error(f"Error getting value for field '{field_name}': {e}") + return "" + + def set_checkbox_field(self, field_name: str, checked: bool, checkbox_container_locator: str = None) -> bool: + """ + Устанавливает состояние чекбокса по полному совпадению названия или локатору контейнера. + + Args: + field_name: Название поля (для поиска по метке) + checked: True - включить, False - выключить + checkbox_container_locator: Опциональный локатор контейнера чекбокса для прямого поиска + + Returns: + bool: True если успешно, False если нет + """ + logger.debug(f"Setting checkbox: field_name='{field_name}', checked={checked}, container_locator={checkbox_container_locator}") + + try: + # Находим чекбокс + checkbox = self._find_checkbox(field_name, checkbox_container_locator) + if checkbox is None: + logger.warning(f"Checkbox '{field_name}' not found") + return False + + # Получаем текущее состояние + current_state = self._get_checkbox_state(checkbox) + + # Если уже в нужном состоянии + if current_state is not None and current_state == checked: + logger.debug(f"Checkbox '{field_name}' already in desired state ({checked})") + return True + + # Устанавливаем нужное состояние + if checked: + checkbox.check(force=True) + else: + checkbox.uncheck(force=True) + + self.page.wait_for_timeout(500) + + # Проверяем результат + new_state = self._get_checkbox_state(checkbox) + if new_state is not None and new_state == checked: + logger.info(f"✓ Checkbox '{field_name}' set to: {checked}") + return True + else: + logger.warning(f"Checkbox '{field_name}' setting failed. Expected: {checked}, got: {new_state}") + return False + + except Exception as e: + logger.error(f"Error setting checkbox '{field_name}': {e}") + return False + + def is_checkbox_checked(self, field_name: str, checkbox_container_locator: str = None) -> Optional[bool]: + """ + Проверяет состояние чекбокса. + + Args: + field_name: Название чекбокс поля + checkbox_container_locator: Опциональный локатор контейнера чекбокса для прямого поиска + + Returns: + bool: True если включен, False если выключен, None если поле не найдено или произошла ошибка + """ + logger.debug(f"Checking checkbox state: field_name='{field_name}', container_locator={checkbox_container_locator}") + + try: + checkbox = self._find_checkbox(field_name, checkbox_container_locator) + if checkbox is None: + return None + + return self._get_checkbox_state(checkbox) + + except Exception as e: + logger.error(f"Error checking checkbox state for '{field_name}': {e}") + return None + + def _find_checkbox(self, field_name: str, checkbox_container_locator: str = None) -> Optional[Locator]: + """ + Находит чекбокс по названию поля или локатору контейнера. + + Returns: + Locator: Локатор чекбокса или None если не найден + """ + # 1. Поиск по локатору контейнера (если указан) + if checkbox_container_locator: + try: + # Ищем контейнер чекбокса + container = self.page.locator(checkbox_container_locator).first + + if container.count() > 0: + # Ищем input чекбокса внутри контейнера + checkbox = container.locator("input[type='checkbox']").first + if checkbox.count() > 0: + logger.debug(f"Found checkbox in container: {checkbox_container_locator}") + return checkbox + + # Ищем элемент с role='checkbox' внутри контейнера + checkbox = container.locator("[role='checkbox']").first + if checkbox.count() > 0: + logger.debug(f"Found checkbox by role in container: {checkbox_container_locator}") + return checkbox + + logger.debug(f"Checkbox container not found: {checkbox_container_locator}") + except Exception as e: + logger.error(f"Error finding checkbox by container locator '{checkbox_container_locator}': {e}") + + # 2. Поиск по названию поля (если указаны form_fields) + if field_name and self._form_fields and field_name in self._form_fields: + try: + field_container = self._form_fields[field_name] + field_container.scroll_into_view_if_needed() + self.page.wait_for_timeout(300) + + checkbox = field_container.locator("input[type='checkbox'], [role='checkbox']").first + if checkbox.count() > 0: + logger.debug(f"Found checkbox by field name: {field_name}") + return checkbox + except Exception as e: + logger.error(f"Error finding checkbox by field name '{field_name}': {e}") + + # 3. Поиск по тексту метки (fallback) + if field_name: + try: + # Ищем label с текстом, затем связанный чекбокс + label = self.page.locator(f"label:has-text('{field_name}')").first + if label.count() > 0: + # Ищем по атрибуту for + label_for = label.get_attribute("for") + if label_for: + checkbox = self.page.locator(f"#{label_for}").first + if checkbox.count() > 0: + return checkbox + + # Ищем чекбокс рядом с label + checkbox = label.locator("..").locator("input[type='checkbox'], [role='checkbox']").first + if checkbox.count() > 0: + return checkbox + except Exception as e: + logger.error(f"Error finding checkbox by label text '{field_name}': {e}") + + logger.warning(f"Checkbox '{field_name}' not found") + return None + + def _get_checkbox_state(self, checkbox: Locator) -> Optional[bool]: + """ + Получает текущее состояние чекбокса. + Используется внутри is_checkbox_checked() и set_checkbox_field(). + """ + try: + # 1. aria-checked атрибут + aria_checked = checkbox.get_attribute("aria-checked") + if aria_checked == "true": + return True + elif aria_checked == "false": + return False + + # 2. checked атрибут + checked_attr = checkbox.get_attribute("checked") + if checked_attr is not None: + return True + + # 3. метод is_checked() + try: + return checkbox.is_checked() + except: + pass + + # 4. По классу иконки (для Vuetify) + icon = checkbox.locator(".v-icon, i").first + if icon.count() > 0: + icon_class = icon.get_attribute("class") or "" + if any(marked in icon_class for marked in ["mdi-checkbox-marked", "mdi-check", "check_box"]): + return True + elif any(unmarked in icon_class for unmarked in ["mdi-checkbox-blank-outline", "mdi-checkbox-blank", "check_box_outline_blank"]): + return False + + logger.debug("Could not determine checkbox state") + return None + + except Exception as e: + logger.debug(f"Error getting checkbox state: {e}") + return None + + def _open_combobox(self, field_container: Locator) -> bool: + """ + Открывает выпадающий список combobox. + + Args: + field_container: Локатор контейнера поля + + Returns: + bool: True если успешно открыт, False если нет + """ + try: + field_container.scroll_into_view_if_needed() + self.page.wait_for_timeout(300) + + # Ищем кнопку открытия dropdown + dropdown_button = field_container.locator(".v-input__append-inner, [role='button']").first + + if dropdown_button.count() == 0: + # Может быть поле уже открыто или нужно кликнуть на input + input_field = field_container.locator("input").first + input_field.click() + self.page.wait_for_timeout(1000) + else: + dropdown_button.click() + self.page.wait_for_timeout(1000) + + # Проверяем что меню открылось + return self._is_dropdown_opened() + + except Exception as e: + logger.error(f"Error opening combobox: {e}") + return False + + def _is_dropdown_opened(self) -> bool: + """ + Проверяет, открыт ли выпадающий список. + + Returns: + bool: True если открыт, False если нет + """ + menu_selectors = [ + ".v-menu__content.menuable__content__active", + ".v-select__menu", + ".v-autocomplete__content", + ".v-menu__content" + ] + + for selector in menu_selectors: + menu = self.page.locator(selector).first + if menu.count() > 0 and menu.is_visible(): + return True + + return False + + def _get_dropdown_options(self) -> List[str]: + """ + Получает опции из открытого выпадающего списка. + + Returns: + list[str]: Список опций + """ + menu_selectors = [ + ".v-menu__content.menuable__content__active", + ".v-select__menu", + ".v-autocomplete__content", + ".v-menu__content" + ] + + for selector in menu_selectors: + menu = self.page.locator(selector).first + if menu.count() > 0 and menu.is_visible(): + # Получаем все элементы списка + items = menu.locator("div[role='listitem'], .v-list-item") + if items.count() == 0: + return [] + + options = [] + for i in range(items.count()): + text = items.nth(i).text_content().strip() + if text: + options.append(text) + + return options + + return [] + + def check_combobox_has_option(self, field_name: str, option_text: str) -> bool: + """ + Проверяет наличие опции в combobox. + + Args: + field_name: Название combobox поля + option_text: Текст опции для проверки + + Returns: + bool: True если опция существует, False если нет + """ + options = self.get_combobox_options(field_name) + return option_text in options + + def clear_field(self, field_name: str) -> bool: + """ + Очищает значение поля. + + Args: + field_name: Название поля + + Returns: + bool: True если успешно, False если нет + """ + if not self._form_fields or field_name not in self._form_fields: + return False + + field_container = self._form_fields[field_name] + + try: + field_container.scroll_into_view_if_needed() + self.page.wait_for_timeout(300) + + # Для текстовых полей + input_field = field_container.locator("input, textarea").first + if input_field.count() > 0: + input_field.click() + self.page.wait_for_timeout(200) + input_field.fill("") + self.page.wait_for_timeout(500) + logger.debug(f"✓ Field '{field_name}' cleared") + return True + + # Для combobox полей (если есть кнопка очистки) + clear_button = field_container.locator(".v-input__icon--clear, [aria-label='Clear']").first + if clear_button.count() > 0: + clear_button.click() + self.page.wait_for_timeout(500) + logger.debug(f"✓ Combobox '{field_name}' cleared") + return True + + logger.debug(f"No clear method found for field '{field_name}'") + return False + + except Exception as e: + logger.error(f"Error clearing field '{field_name}': {e}") + return False + + def fill_text_field(self, field_name: str, value: str) -> bool: + """ + Заполняет текстовое поле по полному совпадению названия. + + Args: + field_name: Название поля + value: Значение для заполнения + + Returns: + bool: True если успешно, False если нет + """ + if not self._form_fields: + logger.warning("No form fields set") + return False + + # Ищем точное совпадение + if field_name not in self._form_fields: + logger.debug(f"Text field '{field_name}' not found. Available fields: {list(self._form_fields.keys())}") + return False + + field_container = self._form_fields[field_name] + + try: + field_container.scroll_into_view_if_needed() + # Используем wait_for_timeout из BaseComponent или добавляем небольшую задержку + self.page.wait_for_timeout(300) + + # Ищем input поле + input_field = field_container.locator("input, textarea").first + if input_field.count() == 0: + logger.debug(f"Field '{field_name}' doesn't have input element") + return False + + # Очищаем и заполняем + input_field.click() + self.page.wait_for_timeout(200) + input_field.fill("") + self.page.wait_for_timeout(200) + input_field.fill(value) + self.page.wait_for_timeout(500) + + # Проверяем что значение установлено + actual_value = input_field.input_value() + if actual_value == value: + logger.debug(f"✓ Text field '{field_name}' filled with: '{value}'") + return True + else: + logger.warning(f"Field '{field_name}' value mismatch: expected '{value}', got '{actual_value}'") + return False + + except Exception as e: + logger.error(f"Error filling text field '{field_name}': {e}") + return False + + def fill_combobox_field(self, field_name: str, value: str) -> bool: + """ + Заполняет combobox поле по полному совпадению названия. + + Args: + field_name: Название поля + value: Значение для выбора + + Returns: + bool: True если успешно, False если нет + """ + if not self._form_fields: + logger.warning("No form fields set") + return False + + # Ищем точное совпадение + if field_name not in self._form_fields: + logger.debug(f"Combobox field '{field_name}' not found. Available fields: {list(self._form_fields.keys())}") + return False + + field_container = self._form_fields[field_name] + + try: + field_container.scroll_into_view_if_needed() + self.page.wait_for_timeout(300) + + # Ищем кнопку открытия dropdown + dropdown_button = field_container.locator(".v-input__append-inner, [role='button']").first + + if dropdown_button.count() == 0: + # Может быть поле уже открыто + input_field = field_container.locator("input").first + input_field.click() + self.page.wait_for_timeout(1000) + else: + dropdown_button.click() + self.page.wait_for_timeout(1000) + + # Ищем выпадающий список + active_menu = None + menu_selectors = [ + ".v-menu__content.menuable__content__active", + ".v-select__menu", + ".v-autocomplete__content", + ".v-menu__content" + ] + + for selector in menu_selectors: + menu = self.page.locator(selector).first + if menu.count() > 0 and menu.is_visible(): + active_menu = menu + break + + if not active_menu: + logger.debug(f"No dropdown menu found for '{field_name}'") + return False + + # Ищем нужный элемент + dropdown_item = active_menu.locator(f"div[role='listitem'], .v-list-item").filter( + has_text=value + ).first + + if dropdown_item.count() == 0: + logger.debug(f"Value '{value}' not found in dropdown for '{field_name}'") + self.page.keyboard.press("Escape") + return False + + # Выбираем значение + dropdown_item.click() + logger.debug(f"✓ Combobox '{field_name}' set to: '{value}'") + self.page.wait_for_timeout(1000) + + return True + + except Exception as e: + logger.error(f"Error filling combobox '{field_name}': {e}") + self.page.keyboard.press("Escape") + return False diff --git a/components/modal_window_component.py b/components/modal_window_component.py index 33511cf..c707cc4 100644 --- a/components/modal_window_component.py +++ b/components/modal_window_component.py @@ -68,6 +68,11 @@ class ModalWindowComponent(BaseComponent): self.toolbar.click_button("close") + def clear_content_items(self) -> None: + """Очищает все элементы содержимого окна.""" + + self.content_items = {} + def scroll_window_down(self) -> None: """Прокручивает содержимое окна вниз.""" diff --git a/components/navbar_component_.py b/components/navbar_component_.py new file mode 100644 index 0000000..51fea43 --- /dev/null +++ b/components/navbar_component_.py @@ -0,0 +1,351 @@ +"""Модуль компонента панели навигации. Содержит класс для работы с элементами навигации.""" + +from playwright.sync_api import Page, Locator +from tools.logger import get_logger +from locators.navigation_panel_locators import NavigationPanelLocators +from elements.button_element import Button +from components.base_component import BaseComponent + +logger = get_logger("NAVIGATION_PANEL") + + +class NavigationPanelComponent(BaseComponent): + """Компонент панели навигации. Предоставляет методы для взаимодействия с ней.""" + + def __init__(self, page: Page): + """Инициализирует компонент панели навигации. + + Args: + page: Экземпляр страницы Playwright. + """ + + super().__init__(page) + + # кнопки расширения/сжатия рабочей области вкладки на странице + self.expand_workarea_button = Button(page, + page.locator(NavigationPanelLocators.BUTTON_EXPAND_WORKAREA), + "expand_workarea_button") + self.reduce_workarea_button = Button(page, + page.locator(NavigationPanelLocators.BUTTON_REDUCE_WORKAREA), + "reduce_workarea_button") + + # Действия: + def click_item(self, locator: str | Locator, item_name: str) -> None: + """Кликает по элементу с указанным текстом. + + Args: + locator: Локатор элемента или строка с CSS/XPath. + item_name: Текст элемента для клика. + """ + + loc = self.get_locator(locator) + loc.get_by_text(item_name).click() + + def click_sub_item(self, node_root_locator: str | Locator, item_name: str, parent: None|str) -> None: + """Кликает по вложенному элементу с указанным текстом. + + Args: + node_root_locator: Локатор для поиска корневых элементов дерева. + item_name: Текст элемента для клика. + """ + + root_locator = self.get_locator(node_root_locator) + if parent: + parent_loc = self._find_and_click_item(self.page, root_locator, parent, parent=None) + found = self._find_and_click_item( + self.page, parent_loc.locator('>div.v-treeview-node__children'), + item_name, parent=None + ) + else: + found = self._find_and_click_item(self.page, root_locator, item_name, parent=None) + assert found, f"Navigation panel item {item_name} is missing" + + def _find_and_click_item(self, page, root_locator, item_name: str, parent: None|str) -> Locator|None: + """Поиск вложенного элемента с указанным текстом и локатором корневого элемента""" + + # Находим все локаторы корневых узлов на текущем уровне + nodes_count = root_locator.locator('>div.v-treeview-node').count() + + # Если искомый элемент находится на данном уровне, вычисляем локатор и делаем клик + if parent is None: + for index in range(nodes_count): + node = root_locator.locator(f">div:nth-child({index + 1})").first + node_content = node.locator('div.v-treeview-node__content') + if node_content.count() > 0: + node_text = node_content.first.inner_text().strip() + node_texts = node_text.splitlines() + if len(node_texts) > 1: + node_text = node_texts[1] + if item_name == node_text: + node_attr = node.get_attribute('class') + if "v-treeview-node--leaf" not in node_attr: + toggle_button = node.locator( + NavigationPanelLocators.NODE_ROOT + ).locator(NavigationPanelLocators.TOGGLE_BUTTON).first + toogle_class_attr = toggle_button.get_attribute('class') + if "v-treeview-node__toggle--open" not in toogle_class_attr: + toggle_button.click() + else: + node.locator(NavigationPanelLocators.NODE_ROOT).click() + page.wait_for_timeout(1000) + return node + + # Если элемента нет, рекурсивно ищем дальше + for index in range(nodes_count): + node = root_locator.locator(f">div:nth-child({index + 1})").first + + # Извлекаем аттрибуты из корневого узла + node_class_attr = node.get_attribute('class') + + is_expanded = False + has_children = False + + # Проверяем лист это или начало поддерева + if "v-treeview-node--leaf" not in node_class_attr: + # Проверяем, является ли узел раскрытым + class_attr = node.locator( + NavigationPanelLocators.NODE_ROOT + ).locator(NavigationPanelLocators.TOGGLE_BUTTON).first.get_attribute('class') + if "v-treeview-node__toggle--open" in class_attr: + is_expanded = True + + # Если узел закрыт можем его раскрыть + if is_expanded is False: + toggle_button = node.locator( + NavigationPanelLocators.NODE_ROOT + ).locator(NavigationPanelLocators.TOGGLE_BUTTON).first + toggle_button.click() + # Ждем, пока дочерние элементы прогрузятся/появятся + page.wait_for_timeout(1000) + is_expanded = True + + # Проверяем, имеет ли узел дочерние элементы + children_count = node.locator('>div.v-treeview-node__children').count() + content = node.locator('>div.v-treeview-node__children').inner_html() + if children_count > 0 and len(content) != 0: + has_children = True + + # Рекурсивный вызов для дочерних элементов + # Ищем дочерние элементы *внутри* текущего узла + if has_children and is_expanded: + child_nodes_locator = root_locator.locator( + f">div:nth-child({index + 1})" + ).locator('>div.v-treeview-node__children') + found_loc = self._find_and_click_item( + page, child_nodes_locator, item_name, parent=None + ) + if found_loc: + if parent is None: + return found_loc + + root_texts = root_locator.locator( + f">div:nth-child({index + 1})" + ).inner_text().splitlines() + if parent in root_texts: + return found_loc + + # закрываем узел, если в нем ничего не нашли + if is_expanded: + toggle_button = node.locator( + NavigationPanelLocators.NODE_ROOT + ).locator(NavigationPanelLocators.TOGGLE_BUTTON).first + toggle_button.click() + page.wait_for_timeout(1000) + + # элемент с заданным именем не найден + return None + + def get_item_names(self, locator: str | Locator) -> list[str]: + """Возвращает тексты всех элементов по указанному локатору. + + Args: + locator: Локатор элементов или строка с CSS/XPath. + + Returns: + Список текстов элементов. + """ + + loc = self.get_locator(locator) + return loc.all_inner_texts() + + def traverse_panel_tree(self, node_root_locator: str | Locator, level=0, debug=False): + """ + Рекурсивно обходит дерево v-treeview и выводит информацию об элементах. + + Args: + node_root_locator: Локатор для поиска корневых элементов дерева. + """ + def traverse_tree(page, root_locator, level=0, debug=False): + # Находим все локаторы корневых узлов на текущем уровне + nodes_count = root_locator.locator('>div.v-treeview-node').count() + + for index in range(nodes_count): + node = root_locator.locator(f">div:nth-child({index + 1})").first + + # Извлекаем текст и аттрибуты из корневого узла + node_text = node.inner_text() + node_class_attr = node.get_attribute('class') + + is_expanded = False + has_children = False + + # Проверяем лист это или начало поддерева + if "v-treeview-node--leaf" in node_class_attr: + if debug: + leaf_msg = f'[{level}][{index}] {node_text} (LEAF, Expanded: {is_expanded}' + print(f"{leaf_msg}, Has Children: {has_children})") + print("-----------------------------------------") + else: + # Проверяем, является ли узел раскрытым + class_attr = node.locator(NavigationPanelLocators.TOGGLE_BUTTON).get_attribute('class') + + if "v-treeview-node__toggle--open" in class_attr: + is_expanded = True + + # Если узел закрыт можем его раскрыть + if is_expanded is False: + toggle_button = node.locator(NavigationPanelLocators.TOGGLE_BUTTON) + toggle_button.click() + # Ждем, пока дочерние элементы прогрузятся/появятся + page.wait_for_timeout(300) + is_expanded = True + + # Проверяем, имеет ли узел дочерние элементы + children_count = node.locator('>div.v-treeview-node__children').count() + content = node.locator('>div.v-treeview-node__children').inner_html() + if children_count > 0 and len(content) != 0: + has_children = True + + edited_node_text = node_text.replace("expand_more\n", "") + + if debug: + # Выводим информацию об узле + node_msg = f'[{level}][{index}] {edited_node_text} (NODE, Expanded: {is_expanded}' + print(f"{node_msg}, Has Children: {has_children})") + print("-----------------------------------------") + + # Рекурсивный вызов для дочерних элементов + # Ищем дочерние элементы *внутри* текущего узла + if has_children and is_expanded: + child_nodes_locator = root_locator.locator( + f">div:nth-child({index + 1})" + ).locator('>div.v-treeview-node__children') + traverse_tree(page, child_nodes_locator, level+1, debug) + + root_locator = self.get_locator(node_root_locator) + traverse_tree(self.page, root_locator, level=level, debug=debug) + + def expand_workarea(self) -> None: + """Нажатие кнопки для расширения рабочей области страницы""" + + if self.page.locator(NavigationPanelLocators.BUTTON_EXPAND_WORKAREA).count() > 0: + self.expand_workarea_button.click() + else: + assert False, "Workarea already expanded" + + def reduce_workarea(self) -> None: + """Нажатие кнопки для сжатия рабочей области страницы""" + + if self.page.locator(NavigationPanelLocators.BUTTON_REDUCE_WORKAREA).count() > 0: + self.reduce_workarea_button.click() + else: + assert False, "Workarea already reduced" + + # Проверки: + def check_item_visibility(self, locator: str | Locator, item_name: str) -> None: + """Проверяет видимость элемента с указанным текстом. + + Args: + locator: Локатор элемента или строка с CSS/XPath. + item_name: Текст элемента для проверки. + + Note: + Временная обработка для элементов с текстом 'Шаблоны'. + """ + + msg = f"Navigation panel item '{item_name}' is not visible" + + ## временно: в навигационной панели есть две панели с именем Шаблоны + ## для их различия добавлены индексы Шаблоны_1 для Настройки/Шаблоны + ## Шаблоны_2 для Настройки/ZTP/Шаблоны + loc = self.get_locator(locator) + if item_name == "Шаблоны_1": + loc = loc.get_by_text("Шаблоны").first + elif item_name == "Шаблоны_2": + loc = loc.get_by_text("Шаблоны").nth(1) + else: + loc = loc.get_by_text(item_name) + self.check_visibility(loc, msg) + + def is_item_visible(self, locator: str | Locator, item_name: str) -> bool: + """ + Проверяет видимость элемента с указанным текстом без выбрасывания исключения. + + Args: + locator: Локатор элемента или строка с CSS/XPath. + item_name: Текст элемента для проверки. + + Returns: + bool: True если элемент видим, False если нет. + """ + element_locator = self.page.locator(locator).filter(has_text=item_name) + + # Сначала проверяем что элемент вообще существует + if element_locator.count() == 0: + return False + + return element_locator.is_visible() + + def check_sub_item_state(self, node_root_locator: str | Locator, item_name: str, parent: None|str) -> str|None: + """Выполняет рекурсивный поиск по панели навигации + заданного элемента, делает клик по нему, проверяет наличие индикатора состояния. + Если индикатор состояния присутствует, возвращается его цвет. Иначе None""" + + root_locator = self.get_locator(node_root_locator) + if parent: + parent_loc = self._find_and_click_item(self.page, root_locator, parent, parent=None) + found_node_loc = self._find_and_click_item( + self.page, parent_loc.locator('>div.v-treeview-node__children'), + item_name, parent=None + ) + else: + found_node_loc = self._find_and_click_item(self.page, root_locator, item_name, parent=None) + + assert found_node_loc, f"Navigation panel item {item_name} is missing" + + color = None + sub_item_state_loc_str = f"//span[text()='{item_name}']/preceding-sibling::*[name()='svg'][2]" + sub_item_state_locator = found_node_loc.locator("div.v-treeview-node__label").locator(sub_item_state_loc_str) + + if sub_item_state_locator.count() > 0: + color = sub_item_state_locator.get_attribute("fill") + if color: color = color.lstrip('#') + return color + + def should_be_expand_workarea_button(self) -> None: + """Проверяет наличие кнопки расширения рабочей области страницы. + + Raises: + AssertionError: Если кнопка отсутствует. + """ + + if self.page.locator(NavigationPanelLocators.BUTTON_EXPAND_WORKAREA).count() > 0: + self.expand_workarea_button.check_visibility( + "Expand workarea button is missing on page" + ) + else: + assert False, "Expand workarea button is missing on page" + + def should_be_reduce_workarea_button(self) -> None: + """Проверяет наличие кнопки сжатия рабочей области страницы. + + Raises: + AssertionError: Если кнопка отсутствует. + """ + + if self.page.locator(NavigationPanelLocators.BUTTON_REDUCE_WORKAREA).count() > 0: + self.reduce_workarea_button.check_visibility( + "Rduce workarea button is missing on page" + ) + else: + assert False, "Reduce workarea button is missing on page" diff --git a/components_derived/accounting_objects/__pycache__/rack_maker.cpython-313.pyc b/components_derived/accounting_objects/__pycache__/rack_maker.cpython-313.pyc deleted file mode 100644 index a8c37637579e6f5dcb4082977bdcb5f7ababcf2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14467 zcmb7LYj70TmF{_IS~D%p=*Bq{oct~uaP_+dH%pB=+bGVt-J(hs zm{BIUQm-!m=4d?!l(4R#piFrq#ksay)jZp^tU@J8b3+<4>fUZsKMS7m3V-F)E{I9L zkZwtD$JJ$j26mCtXzw^NX(_h+$84KVho8fTC9S^DzsQ7 ziB)Pb4~coSn3u%7BvvI^0V@@ER6emFy`|2 zwUCHZ?WMaB%w2NZiEGQlg>ZCjPHvwUp)Y)Y|j*}QqP;uzy6Q)d-Q zDHC}i7Ktn6vBY^8-=uI!aYmC96FAoHXykO955rIR>JU?r6t6gv7ZSXnxS`njSdl6>DJ0MF(bVhIpubw7!(ps699CT6@I-QK3PgkzCLDfdDiYUHJmK(}m>{O&F__dO zBs;_5v1Al}*u<2eKyoCJNJ53NWI_yw1uqPQP>n$?26Y(JW6*%X3JiQ0G-80eL8%Le zvB9xuIFd>UvC~s29tAZ)+Fkfh-IE|V&^`LtP;g*W`X#K{H(-=uL4OEC`yno0{Q4{D zRTd`Ty7by8tnpt;uS!1&kPU&W9~L@H#5_#Go1=$AeF5o;^adIG_d(KgknlFVzmMBw zG{Ij8pXUV;x-*)X5W~=zRCqETxpYCmNg3rk@ar>?aAYc#oFLsEg;`M7yciyXIXcge zjYgu;A{8hq0OS86hL-$GRl zmUw>+BKg)3{M&e7-W zLK){Ab#gTPFCV2zI?cHt%`TL4?opRof@8=Rcfpqm@`d5-qx3c_S4m^MWj_ zr;Mv2Up)n-s>v5GSI5qka^J`?D74&D0c=vHPfZDl!KkLMp&p7t^@jiN zu7O~WOQl|ZIf~3{UB|W(Of_|WS zD0#lFZs<>b4(>mrPP6gOuZjDtHFdQ!`PIo%1Z(bxE#;D~5$k&kbkqm(Wul$H44zJ& zPF_UIjNfkt3e``3BeUxPIiL>uC5U(%tvRYZ>84grzD(2=+3ZHvYeX5pfJ$4|}OCIW2F`A95|sE$>8)6ITaW0EoL>5at!oQHQ=`IkEt`D5=h$SYcK$S-(C;7w}!~if31Cn<@6QX>&t{sHN z6`nAD9Kq*sC0*ZsjQBdshmScxsjwilFW=e zlQ|(sAyM+Bvdq*c)lJtg%%*4K($+&#b8n{lu*4kxGhKO^d6BvD9ZZ-D+^)Oryq%Kj zj%4YhOJ!8~iX6RFrnhG4Z3{G$qnl;A`Fb=`+)4o}|gzvS*LhqGPtsOabzs&B>v3)Yz_tDx6 zdrYF60U%%VU-jpzSIgC_r8PS<)!n)3eRB1_8K>5eHkoc)V7!<6UhK;;%`(&ci+HYO zhupFw!|cp4du3+t0#kL*VJmkn(fO8RPZns;<%$<8e(2Fh;==Xx^|-WQuT;114&9@| zl5zs($ywPz3@|Z>83_9L0bmk9ACwvT(DiS^8nOfnOG9CNbUYH-2lk8lDjZ@~608H( z4iBJ=GW8clnIRPJE9wSRQGiCkCR>7pG0|nI?+0ih;1six-=KvKa|s0Z75~m`hmJK0F_kJP**m3XpD6@*A|T(p8y$q;qj-We$qw@C) zvJr9&vTea^uL3m+01le{duTS&OFx!hgb-O%I1<09Ayk7UO0P0nr?G1+uxpV-EKLX@ z5Q}V=^ee&~5XQAr`ez(KBxlnrjKUfl*O`i?;(Rwd=yxmLFmkL2TvSvbs*KukkS8F? zDD1}oN8u|OHNQ;-F&t(>{Gy%6HSkeq20lb(?Sr5XF^uB=U4T>Rt@DD6+j#(=+Yo&DuZ^v@IC*sYBOME8nWv1h8*lXecIfpXY2&W>1CnoFmg&*z zW^-<~xO8ssu)KFzI>Bd|Ge-F>bN<}c!}8X{(w0XhUtgB#UtV+feBXyd?+kqe6+dYd z%vUV=c4wJA%Zu%p_kY;=PUlB^v&_iX6@!KrxAk#pWGpu_DUVDp!3bOSTkla+#R01! zqkDd5?vZ2iBgdrf$P*zz2h zsiXp4W%xmPl>$r4*+vYk!rAk`fvdCuTWRGSz!m~qNpP2mdLxz9YgbcO=>0Joo+!V{B; zZ2@zEs!w%tya7k+`~g3ZuGdekDgGiHeewJw!Z9`7kU#boq;#|S6Is!6Me+@%i@JLX zmgXKSKyHiP-7z4G?$Z%Vh(bEAh~TgdmzhEC5T$n-mXFn_1CEFVbr`guu{*$bMe1;j z7&IZsHweIdK7EtuwSW;XpbW=tA#2c#4$Ko(CJQWK1A5Yg^waC&oZTlWDpEX7W__T| z#tCHO$d^32Y-5DFi$=&yfv6)?UN{RNyRJkqM0MFLHS7$cfh~E!WeSD~AAsF9GwO?T z>p6h z5-?wHBNl{$8ht0dA4s#@)3M1(B(C_0$*gii`dTS|t7h>{amk{7)HG>-)r8e~r1yNUQeVcFlkL zqb*YN(X4lH!CMcwZdD-HvPEv$GPfbqvU_IeFEwqLyYgz~Y{QMl8;rDVBs=<)6q(FL zrsT*}CUPM&dQlp=B&|wkz28zrAWODIZrn20o@wmPHSUue_st9}dKz;cR`#$NPupxH z<7wB#O0CDG;HY%`N$JT`(yDOQ`;;oAc2%y%FW2~UH5=ucjarL!{_F{5{G; zl~xUAy+djPDpsgWR+d?{z%;18uRon-HWbI8Cd}C+Gfme+S!T6{0k7|v?VsIsL!8?_ z*Dm?G?=X)L@b@{=MBsCLU%veLO8og6Y-@CtzXP=XOVT^YVWZ0wK7QV+X8iyTNW^yv zJ9>I~F7*6pepV=kK30+sB&Jsl=Z(ENv+OF^BcmXwU~9Bt6s=sz#R9**;Ixy(baLbY z0*-o$W%sFsYn6KDDOj6I>wTB+^B_X}PYC`OoRh#+dXN~E>N>J?0H?F??4xmoQbXW3 zskx!twnyb{k7l;@XF3O@z!9nLXqFyavTrEg^=om7g4bJJU*L=UUV6d;=cdbhF7NpR z(+IQl_`jTb?NpZ8236?CDM*l3!pS1n-7k0dXPE&4{abSG7Lu~(c6n|eC-393%&>7N zASqpQkLR}c%G-Oh2C*yEJ#>eDSOxx?wS*q3pdanQ-T#AtP?z|%PyQ9rfxtd> z7uxTF8ztbxlAi~wrJ1@nfFsCbSi_eCp7Ii(dyrfR(ELQ`dRpdr`WD(lQ{PuNtKFzl zo&C6dZsKMn+Y-na%{BE858{l%+X6)b3fqh#i#$}W&v33(1G}OZ2naTBN)7*%-$lGB z0x?vG6_wiFq2SSQ-{7&(aQMK`;BZghAYAyeB~oXFag;|$i#_GDIywu&37W8T2R4%q_^jlCJqK8fB(Hk3LlsH38V&m z$zPML30ptp;2a%rNJ%(`>>-!hgOmP8KyCzjAF?K#L(Y>wgFJ9b$J!E3XxG;tYD{i4 zgq)mnrzPPIxxemc!&2Iq!OA(RjvH24@)VBIIPEW=^1}qVzy!Wah`aaDr3<%iVB3EP zKk$aoyLeyd7K@t_F7V-Il&3eTn-a)N+)hM6pb5RHd!|8!z|9@pw{J;rlB+zw2aEWe z{6ok91S9cWlhd*MBd8E>DkUtp(Pz$w-;9x}zO#@QKGlr-z6OoIE zo7Gq)Tyq6o1Tpn~k&FDqwn7Vx&!SQhoe~7#pH*?fF{njEGX(Bj<54d=4PXP76RBp7 z=TA|}A_SnIswvPXl`V2*%k>MH%Jv!CVtvDmd!eRwhF)k`F~cnQR?bu|R@P@K*8Z;R~PlJRxT^ncQ{YS#7hp_!wL zo~DdvC7N@4J?~ znw&kCso#>T-yzrUSn#zk*{Ft{OO&;ta^^4~l#1FM6Ofremf5`Ir#uaJD%UP;D)6V? zq1%>ojD|kMTZZ@N7XcbziIS(-%K>`PM8oBEDjc0nwOe71`y7l5n8tA%fQ09uL1wpL zV5{JD6hzOU3$$x0rMC-907L$go`9D`(XY&|M-zt^jB%&`q(Ek2Sy#G7C(^JaizUIg z(u`$E#2jWj3Sc&Y)KkzI;YkQIBP?+6yU^?^m3XcU8_iQ%u!@39mVx4Q2WV0eH>G;7 zPle768*c3U?Vi{7NE^DPx<|6~u0^~<&?3_<*DqyhKXLw}<9>E$mhR9Yb=|f*^!DX= z6{njR)pMZY@4tw297+~~i77D5oaKJYpy2YK?F0w=aZADqZ#%q!6RM#e zg>g#QPxhdx90L^GKyko5X^w9Ah7WH!Sxbzj0~WFgQbyxT$~b{@LbwDETgXk~Sd~0l z3B1?`vxN^!9R)7I4eyW@+BKPPS7eByj21Y|%(gy)21~Z`zRX`aX>oD=yB@HVtEknF zJzyz!Q7J*~>wRY2U<9@6Z@+_qg3|#psBjVni>_XOgH2Cv>>@vAoD&eds{jJrj{qXR z3;$n0<$&-(R}N^v0pr8%H8A;wcg%W3a4+J``epabzyL%3Thh%AwD90H1(J;%4OCEH zjO?Xbt~7f9FH|AhMHGd~xU7w{i>5K#zqG1}89pVWR{Q zgA@iZ_LKlbG9^5Y!4nwZkb)bsxM12FFRw1z`>_C}pPdv^}z-6tX?nVS&>oUL*XMqyL zLq!4Ca0sJkA@G-}@B%3X#lv5mB#v*5-cme*)*tPIO1dH6CITL&RB)gY5RK}6Hljb( z5f1BPuWTq)rC*)59yY4D8G=H!TK904-T}tgoRR4G&Dh-DT-RZ_>u{#4FB9mOItJAH zTz@NLR=Q?9e`}#up0dn%{?zE7>0hjA&eUv{m{Dol!P`&VJ|Z0(k+zOzAYQY1v86+9 z**P<`i2m7DnQ5Kvl(rwbeeO1}Wv8UB5*Ka%ja|~i{gQ9sW9G0WP&~ej zXVXI47P)Qr70*IVTdt-I5lxn0uUpw!BJTs!>dlFuik z*2g}sJ^5#yHtd{p-T1cj$e`3b^s)EYU#p;^d$k5(^Y9&dhk8D1!YQ>18{swRuN9i% zE#Myq2*{ZWEC#I@Al@K1*M&CtrP%QdFC52@i2I146o|jw3*fY48P8G~yWtbstO z(C)-Xc`%(}zeU&#UqzTbN~NU))RiQ_Mu-T9{{DUH5B8HwcG|J(`tTA3ui3FL@U^sJ zvtz&I2JlDtp6^oM((#b?Lbi`Wi=&{VMxkur9Y5h9K>Kl(OQSh2SKwh^&0K}G#St=Y4-Jq8!c+Cyp!`ByW z+Z{XqMnRyCF7{>+Xl@)r#Z4~9;UALmqJYLnsmcEpO+dVqfIs({jHx4y1E?^mWHK%W z)H|gD?jNOIJ1U3)vTyX5dgs%Z|Ert8Y52o4yiKY86^=kdVY0auTI5ql`15>Roj5Yy zxCqn+ufs~{UFu&x?GavsC{B(TgkZ^Lu~_~h4*W=EQ;lZ-q5$5eEk zaeuVu+@@yTf9H4ocJlh8C0}kFno+}5d z2AsT;Jkx{K12udNdA1L_25R|Q^6VH~Gq9Fl%TQysjTBqeZBlx{*NxQ${aV)wch;%p zV5kNPN>qC(wq~rR+pLyzPgM9_nPK=R{+W1Pd{25w{FU@NBfTtL65kg;5I+>NjPwGe z{6c(B%!=2a8wx|C9G8c+*^i+FT=+kqu?V`dMd9uTkgFHD{%UI>k3bu;;t75&Rlyl4p@zpzR_{q*% z&YJidC{aW5xLB87Z!L+bHMF#b#H=yItR*pP**d*cJ&CDf-FnPA5>pRtc*v8RZQ$31 zs68g$vx)LI$(}x*i=?@}Gx0>MKf#@YZ*Y*0oZ~)O3uBVm3xW6-+MM}fGe)*d;!Lse zTsp%ihoTiwr*`3D!zp`j7sNhgdXpN}Qm7Fa^FM<0kf|`$N43QXv8<_Ettmt$)M-yq zqjs$V$WdX)p_d%3F67W^h8ncFK!vDat(K#357rf)wI|lBl@GeLxWXO5p~qCVoK-L9 zDdg9lSeurbtoYCB%V>jEd(f*r7485Z`WSz~K3D2}Hbb9ng}Sw;V6*mAxQ8lOTKhr_ zj{C!wGqd(Wxe15AD)X06t3#Y2IQA`Z9uVzU5dH#C?Ghm0`_hXBb>OgrMAzn2kzD*h5$LX7+fF!r)^2^x@Igf*kU>bIoVm}7~^#S46V;!N7{X(x15 zd`ClG$oP6O?#_!Jl%OsnUj1|vmiiIARTe6Lfq~*blfDg&{t`i%kzOLO4Ue)T895)H zh#=4cmeOY;LO9JmpO$UonRGgpg3?7cM`AJA7L6pMTw=)YlC45!lH+AM9F8aB z>2R1w87Wt$74YS&@L#o{p?DYonWr(%8bJi-*Why%%7F2UVLp|B6@NOONJEY0WcrL; z!Go}f0?fxDWi2!v#){MN$*|Hfzg4zC<)@W;NCqVVL<*5GvRmn@nqQB$hr`iCL=eJZ z0oOQwpG(UN@YL5kc6>NEI3~UYqjePq4g}ms@KHX3@0G>aAB%4@Fs3u&J7X}CZ;5Y< zZv=?6L}~aUh{8)Shpve~A3GE}5)iM5S1}SLC){yzWhIzu(&e!vcOiV9;{_PC`#$X~u9Qh`KD8b`!6ahI5qm76h zCTW4mi##qPnOOm|1KOuUI1=tb@P|v(axG=Ad9_la8)oVk=$2(0)zCg?5d)E&XMEc7 zy7PAPj_gjcC!A~k%Cu#H_TF~4%^t|P{gCEZvQaG?e|++XC-coaB*?cr*St61d`N0O zwCFy(WTsjUo0h1G7RNt1+^=rEawg~4@DuYp=GlFJZdf)miH;Su+TtT93YX6kG2ADxz92L?Ff`y0xS0P1v3RP(3ljak3Pr`bR0I(N? zXAoCo$;Dd8+gOcnGhsW?iK`}Q30caCs&2Io^4{fyT&30z$YBYV5ocNr?P^W2;(nGJ zb)}zlaX+EhcG4QM2Ac~1wI{7ltfTi07&*3R+6CF<8v)U2g{UYuRSV=o87Vb+YQ2w%V)wZilM%=}VAX0xQ``Ft|Cn|| z<9@?S7@7mgYg1U0Y>jc_nF&P#R;pr;N2N%vKR6tGCVXV*SZE~NH#{`5@5oSpkZ;4J zMija6daWT2}LIk8ebSRvT zpW{-QwCsorkco^Yj6$3j(y}!QLx#6RJo;6?jrT#zd;ptY6HO)4k$95hwRUA!X+M;R zu%1(PlCf7MxqxqoVw++3typC}8D7Y)47~DMa>q(SuUzh(P>;aE1X1Ll3&n)!*`D=g zo%07o_dXCFAlsT-e|+SJNAl~pN$a=$RU#i4mIA}M^~dt-N2K*5V(7Wt`r{(qxae)k zdv{6RU1HBb&O12mx?APWRdw8UuDv|;>d?&b*;BJe#qE7!(}4wN|Dtm(RNS;h@^(+V z;K|;Qr*}y7jsEEYZy~(FJ-Vq}6W7yZn;NpLcDQTwAfeqpu#Fd1AJAwnGdI ziH*Yx&SOe_4Zgf5AbA3F-^h9PmcRn1aWbFON`Y-_HnO|3(0>Y3j% zzy8LvV&kC&=V8cL-I#Z_NY0ko5fSfO7M-B0<7bXhvJgRd9(-5^7_;(Eu4U|0TCB`Sd0na~!^sA`SNw50?Q%!w1hjHDel8kR( zEap>}F5(WdqoH>#{FmU)KY>9D1UO%zj5=T6HIeBrV1LvZTdSm_R;%EN+`jT`#hMB^ zzXG)gxR7C{!gNt)svG7@m#*EkN_6Y&=38|8|EiIH8^E5Jjr5UkDH{1cLOhIvkCJ+b zKLQ~?h@)Z{;5uc1ix_~sb@eeW%6u9tylF03@R!o1xXBo z`1_Ag7jPIL$sqTkGbovnAgRptI<6^D2Eal{vLGP>yP&2UfU;oh^mT{7g5LlyCwov9 z6;VVGDrumHrQeeXEhQrt3#Z1{?x zclAiFo}BARk$#eVQGP#UiQ;F}E>zITXZy(%UH713Q3P6vxtW#L{5FuY&XdumxO2;}5trq|2xzbTDYRQArOvIY3!p z2*_#bCZpmaAR?OA5E-$C>pHCw-~SKOp^pmP0;HWMMnzpa*7RtmU3HUmFsVtj=S338 zwf+>k0_cq0k}jdBxK2DX=+uGtK{*1pPGY?JPWD5=?7);ZA ze3IXb=~W5?BOUNN3;c)(9lj6iIe__811d-`@;0iO3M=Okrxb%NNm6ShT#CmfBD>I- zYiLJsLM-7;YM{_37tl9S3PK!s*|(BJEy;KusuQxXZa@`n&{R>+mbu=!j%-qNgT?J2 zatyp6KS8CWhT@4kj+njfI zO76~_yDRVR$@0^V+jX89N4}w5YG}_jY{}Jaou(mQeQUn%F{$pcTwVKIr&QOGuN%xZ z=R1!`okwz=gVXfC099*P^!oGOPRZMu_2s<1dGB7yyLWnU(dEs%7|F%tTpMSf&AA?1 zZ1Lw?x}}!xT+0*rmOb-3u2kLL_}J_p=Y6{*->#gmH@ET0E3VtM>we(;o^$5H?9^;R z+;QlpOFVQ!Y(BYAdup+%E#LH|*>7H`TWhZqWk0m zed?~ulXtbv>=Nm=`xRES>7l&8cN0G6qbRJSx+YW38>1n4(gZq>?rDQ|)T`jD1H)xS z5M2f(SHaViG>6Q(Ok4*hIGoMo3YIZ=7V|&W0tU;6R>DKN1X@i-v({da>u566d$k>8 zskxk5KdS2{!FsLNg}XjVHqbb9?W2&+g8!JMdn%C08s55$Zc;FB64vX;ARvJy56oyF z&|U*5HgE{^ZGbOd`Yssxh!g1r5OZL3LwTXw*#f*9s>mSD>4q)e!#I`_~WjXk=t~h~Eb}2(|ZDDlm<-fFQKIR?`6trmAZ4k_uHx@v;*zlf+>6o z1jJsFY9u$hO-tUf5+<))=BH4<@Bsusb5^unm9tkZHh1Kkw@c02#m?io<`W|A{n*v2 z*?byD7Mvl)%G7w+G+n*uY5Kv%?_JD$+9gl>TyxIzMBcMU^6bfZp1#pa>`ouMn(lf# z^4^`2cjrR)k$g8Nb#poIX+T4#ClBJm**tq7y9wa`*Ul$28&c!0Th1PWQ?f&G*RAZD zxeM?3h1eJs)V=&rg?edvFX)QpJih{e*4N~RC)z@9Pz!uvR9mNuvzZ~LRrz` zhV&G+86bltn-5@tO4EP@KSSQgv=a>gx4NLGZI4D7^7;BFl70w66#M#}HN0GH*| zfRC`nWQtL@UkJ>Fc=`;Z6ed{52?3`6`AM)^f)_BI0>TTPNj*30@c;^!oB-yzl`!Ba zs3(vYU{7xv${8z7B~tv*yG{cXmSd)blycBV*-;pBMdzvO4+e*W;XalPv#{^6Z)7;g z5|!v(Rn0+fT`3!_A+MzEG7)4uGE5RD+^5YnN0H#G%u*?5cp15SC52}7V$dn!V+e{U zr*FIv+WxVtVTm%^9g8*Y zD`&5aEm5Y8UAMaq<-10uu2FHv*pk^){bkcVGh~^*xD5MMh$lxOZ{n7a=zi`Nef)mx z$6+Q!ZTbJPABLiEt&Yk%#d>7yk8~KZe|5}KgASegOCq?(E1$y*<2q4$3Hv;nPy1ANs`);Xycdq>@ z(f2d}^j=foc3^)#@SGHQPTY1J5&EQx(BHTZp@&8Hv0L=B3Pr8qRN1chB#U+s>WH2A z;)q>_rm_Y?2}BiJDY-O#7PNCyIpdp_4u-cflvYc95^6QLOj?QaYSj{}w}$lj;K1+SnAd zUts;8+kG9(p%l1Qd^~Q;;U^)O+PDJYXWGFkn2CTN$~RTv^AGv02BjByJ`ejb!NHSA zU4Vl`7KQV_fKg7Rpt?_hMoSzj+JXfV(eS3YKbjPHG#UfcEeQaOU8@6Q*+QFOT!g=c zHUJXW>28r-^DW}8!#6FW`8P08?0y3&b##+3(6$yGTSQ!_T~cn zrTYD!Q6~3((_-C*eBEZLZgZ~AH}_P2>(kQKr*m8PNp<^RW3+t@@jeiwTdFX*yo=s; zvC%)*nBAQX+%O@g^{JTlypCy$HS1@-EUxbW4BXbZ*tmJ7W_BENZuu0|(^7~dJ2Z+Q z4macxrSt|od_hYbew31{9H_BoJaLk&II$LiB&L9em4JYafI#IVfQJGb#q=8$tbc{7 z%fSW#2iWfl)LU`!{*r-BVsKIpw~V1ht~@S~0b30h(2=@YwGRsh_t!(n{UaklqmBUT zVGf48f^M)w9|r7rty0cwkn>>4ifI9>;Xm5>3^%_paBw(F+fa&b zv{HB=8%8s=&|2wEQ@#lzm6Vd5ko-%Kf?|)X1&=a8)w51yyF)w?PhW&x(8S)NCg5y` z!&3YJlqJ^i`?u5yXHpqB@-oipOG_y$th}Xe#UiJ0iSW=C_?il5Z#pEpV=iRen(CKm z|J-J=vu}Yupl_rk*-jX@R+4P(;?9E$^dUVP+1A>=KyQT@ZDaH;dLvmB00j)bDefvH z;{?Wk^tlfCO+)?$Jfnuz%WznBgu~}ju?%`0WoJ12HBcF0`K6qS*{7DQV7!bDv4hR2r48DoMGzOP3cmo32 z4d>6&aJDNNj-+8*2n;<3N7CeK^>iAXu}g%*eiOe1GLxOHdx^TjqY<41_+ZR( zWeRS;ukW^QnrmC4;C91DZer-oIfYCt{L^oL-^Uw&~Zfsh{+fuK`+Gol_9Ny<467Qew za9G>^nSy|PVe-!Yb^MR;{Wn1%*A 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/modal_window_locators.py b/locators/modal_window_locators.py index 3b61298..d30043c 100644 --- a/locators/modal_window_locators.py +++ b/locators/modal_window_locators.py @@ -23,15 +23,8 @@ class ModalWindowLocators: MODAL_WINDOW_TITLE = f"{MODAL_WINDOW}//div[contains(@class, 'v-toolbar__title')]" MODAL_WINDOW_TEXT_FIELD_INPUT = f"{MODAL_WINDOW}//input" - INPUT_FORM_USER_DATA = f"{MODAL_WINDOW}//form[@class='v-form']" - TEXT_FIELD_INPUT_FORM_USER_DATA = "div[2]/div/div/div/div/input" - # TEXT_FIELD_INPUT_FORM_USER_DATA = "xpath=div[2]/div/div/div/div/input" - MENU_ACTIVE_INPUT_FORM = "//div[contains(@class, 'menuable__content__active')]" - MENU_ACTIVE_ITEMS = "//div[@role='list']//div[@role='listitem']" + INPUT_FORM_USER_DATA = "//form[@class='v-form']" + TEXT_FIELD_INPUT_FORM_USER_DATA = "xpath=div[2]/div/div/div/div/input" + MENU_INPUT_FORM_USER_DATA = "//div[contains(@class, 'menuable__content__active')]" LABEL_INPUT_FORM_USER_DATA = "//label[contains(@class,'v-label')]/span" - TASK_MODAL_WINDOW = "//div[@data-testid='BASELINE__dialog-drag__modal_0']" - - CHANDE_PASSWORD_WINDOW_CURRENT_PASSWORD = "//input[@data-testid='CHANGE_PASS_CARD__text-field__current_password']" - CHANDE_PASSWORD_WINDOW_NEW_PASSWORD = "//input[@data-testid='CHANGE_PASS_CARD__text-field__new_password']" - CHANDE_PASSWORD_WINDOW_CHECK_PASSWORD = "//input[@data-testid='CHANGE_PASS_CARD__text-field__check_password']" diff --git a/locators/rack_locators.py b/locators/rack_locators.py index 6b84059..b84159a 100644 --- a/locators/rack_locators.py +++ b/locators/rack_locators.py @@ -53,6 +53,8 @@ class RackLocators: # Чекбоксы 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')]" @@ -122,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/pages/main_page.py b/pages/main_page.py index 4a79b3f..7643504 100644 --- a/pages/main_page.py +++ b/pages/main_page.py @@ -103,6 +103,16 @@ class MainPage(BasePage): node_locator, item_name, parent ) + def click_expand_workarea_button(self) -> None: + """Выполняеи нажатие кнопки расширения рабочей области страницы""" + + self.navigation_panel.expand_workarea() + + def click_reduce_workarea_button(self) -> None: + """Выполняеи нажатие кнопки сжатия рабочей области страницы""" + + self.navigation_panel.reduce_workarea() + def click_user_button(self) -> UserCard: """Выполняет нажатие кнопки пользователя.""" @@ -207,6 +217,25 @@ class MainPage(BasePage): item_name ) + def check_subpanel_item_state(self, item_name: str, parent=None) -> str|None: + """Выполняет рекурсивный поиск по панели навигации + заданного элемента, делает клик по нему, проверяет наличие индикатора состояния. + Если индикатор состояния присутствует, возвращается его цвет. Иначе None""" + + active_item_locator = self.page.locator( + NavigationPanelLocators.PANEL_MAIN + ).locator(NavigationPanelLocators.ACTIVE_CONTAINER) + node_locator = active_item_locator.locator( + NavigationPanelLocators.SUB_PANEL_MAIN + ).locator(NavigationPanelLocators.TREEVIEW).first + + # Рекурсивный поиск в дереве v-treeview заданного элемента + # и клик по нему + return self.navigation_panel.check_sub_item_state( + node_locator, item_name, parent + ) + + def check_navigation_panel_verticall_scrolling(self) -> bool: """Проверяет возможность вертикальной прокрутки панели. @@ -233,3 +262,13 @@ class MainPage(BasePage): NavigationPanelLocators.PANEL_MAIN, "Navigation panel is missing" ) + + def should_be_expand_workarea_button(self) -> None: + """Проверяет наличие кнопки расширения рабочей области страницы.""" + + self.navigation_panel.should_be_expand_workarea_button() + + def should_be_reduce_workarea_button(self) -> None: + """Проверяет наличие кнопки сжатия рабочей области страницы.""" + + self.navigation_panel.should_be_reduce_workarea_button() diff --git a/pages/ztp_templates_tab.py b/pages/ztp_templates_tab.py index c18cb06..dd6c001 100644 --- a/pages/ztp_templates_tab.py +++ b/pages/ztp_templates_tab.py @@ -8,8 +8,7 @@ from playwright.sync_api import Page from tools.logger import get_logger from locators.table_locators import TableLocators from locators.modal_window_locators import ModalWindowLocators -from components_derived.modal_view_ztp_template import ViewZTPTemplateModalWindow -from components.modal_window_component import ModalWindowComponent +from components_derived.modal_view_template import ViewTemplateModalWindow from components.toolbar_component import ToolbarComponent from components.table_component import TableComponent from pages.base_page import BasePage @@ -44,16 +43,16 @@ class ZTPTemplatesTab(BasePage): Args: title: Заголовок окна. """ - self.modal_windows[title] = ViewZTPTemplateModalWindow(self.page, title) + self.modal_windows[title] = ViewTemplateModalWindow(self.page, title) - def get_modal_window(self, title: str) -> ViewZTPTemplateModalWindow: + def get_modal_window(self, title: str): """Возвращает модальное окно по заголовку. Args: title: Заголовок окна. Returns: - ViewZTPTemplateModalWindow: Экземпляр модального окна шаблона. + ViewTemplateModalWindow: Экземпляр модального окна шаблона. Raises: AssertionError: Если окно не найдено. @@ -92,14 +91,14 @@ class ZTPTemplatesTab(BasePage): row_locator.click() # Создаем временный экземпляр модального окна для получения заголовка - temp_modal = ViewZTPTemplateModalWindow(self.page, "") - title = temp_modal.toolbar.get_toolbar_title_text( + temp_modal = ViewTemplateModalWindow(self.page, "") + template_name = temp_modal.toolbar.get_toolbar_title_text( ModalWindowLocators.MODAL_WINDOW_TITLE ) # Добавляем модальное окно в коллекцию после открытия - self.add_modal_window(title) - return title + self.add_modal_window(template_name) + return template_name def close_modal_window_by_toolbar_button(self, title: str) -> None: """Закрывает модальное окно через кнопку в тулбаре. @@ -108,17 +107,7 @@ class ZTPTemplatesTab(BasePage): title: Заголовок окна. """ modal_window = self.get_modal_window(title) - modal_window.close_window_by_toolbar_button() - self.delete_modal_window(title) - - def close_modal_window(self, title: str) -> None: - """Закрывает модальное окно через кнопку 'Закрыть'. - - Args: - title: Заголовок окна. - """ - modal_window = self.get_modal_window(title) - modal_window.close_window() + modal_window.click_toolbar_close_button() self.delete_modal_window(title) def get_rows_count(self) -> int: @@ -142,27 +131,29 @@ class ZTPTemplatesTab(BasePage): def scroll_modal_up(self) -> None: """Прокручивает содержимое модального окна вверх.""" - temp_modal = ModalWindowComponent(self.page) - temp_modal.scroll_window_up() + self.ztp_templates_table.scroll_up( + ModalWindowLocators.MODAL_WINDOW_SCROLL_CONTAINER + ) def scroll_modal_down(self) -> None: """Прокручивает содержимое модального окна вниз.""" - temp_modal = ModalWindowComponent(self.page) - temp_modal.scroll_window_down() + self.ztp_templates_table.scroll_down( + ModalWindowLocators.MODAL_WINDOW_SCROLL_CONTAINER + ) - def check_ztp_templates_modal_content(self, title: str) -> None: + def check_templates_modal_content(self, template_name: str) -> None: """Проверяет наличие и корректность элементов модального окна шаблона. Args: - title: Имя шаблона для проверки заголовка окна. + template_name: Имя шаблона для проверки заголовка окна. Raises: AssertionError: Если элементы окна некорректны. """ - modal_window = self.get_modal_window(title) + modal_window = self.get_modal_window(template_name) modal_window.check_content() - def check_ztp_templates_table_content(self) -> None: + def check_templates_table_content(self) -> None: """Проверяет содержимое таблицы шаблонов. Проверяет заголовки и наличие данных в таблице. @@ -265,10 +256,11 @@ class ZTPTemplatesTab(BasePage): Returns: bool: True если скроллинг возможен, иначе False. """ - temp_modal = ModalWindowComponent(self.page) - return temp_modal.check_window_vertical_scrolling() + return self.ztp_templates_table.is_scrollable_vertically( + ModalWindowLocators.MODAL_WINDOW_SCROLL_CONTAINER + ) - def verify_template_data_with_api(self, title: str) -> None: + def verify_template_data_with_api(self, template_name: str) -> None: """Проверяет соответствие данных модального окна данным из API. Процесс проверки: @@ -279,7 +271,7 @@ class ZTPTemplatesTab(BasePage): 5. Выбрасывает assertion при обнаружении расхождений Args: - title: Имя шаблона для проверки (должно совпадать с id в API). + template_name: Имя шаблона для проверки (должно совпадать с id в API). Raises: AssertionError: Если: @@ -289,14 +281,14 @@ class ZTPTemplatesTab(BasePage): - Имя шаблона в модальном окне не соответствует ожидаемому """ # Получаем модальное окно - modal_window = self.get_modal_window(title) + modal_window = self.get_modal_window(template_name) - # Читаем данные модального окна + # Читаем данные модального окна (метод теперь в ViewTemplateModalWindow) actual_data = modal_window.get_modal_window_data() # Читаем данные из API - encoded_title = title.replace(" ", "%20") - url = f"e-nms/DHCP/showOptPattern?template={encoded_title}" + encoded_template_name = template_name.replace(" ", "%20") + url = f"e-nms/DHCP/showOptPattern?template={encoded_template_name}" response = self.send_get_api_request(url) # Проверяем статус ответа @@ -308,5 +300,5 @@ class ZTPTemplatesTab(BasePage): response_data = response.json() template_data = response_data['data'] - # Сравниваем actual_data с данными конкретного шаблона - modal_window.compare_modal_with_api_data(actual_data, template_data, title) + # Сравниваем actual_data с данными конкретного шаблона (метод теперь в ViewTemplateModalWindow) + modal_window.compare_modal_with_api_data(actual_data, template_data, template_name) 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_element_rack.py b/tests/e2e/elements/test_element_rack.py index 0f0ba22..f1f173e 100644 --- a/tests/e2e/elements/test_element_rack.py +++ b/tests/e2e/elements/test_element_rack.py @@ -5,6 +5,7 @@ """ import pytest +import os from playwright.sync_api import Page, expect from locators.navigation_panel_locators import NavigationPanelLocators from pages.location_page import LocationPage @@ -19,6 +20,10 @@ from components_derived.modal_edit_rack import ModalEditRack, RackEditData # Константы RACK_NAME = "Test-Rack-Functionality" +# Инициализация логгера для всего модуля +logger = get_logger("RACK_TESTS") +#logger.setLevel("INFO") + class TestRackTab: """Набор тестов для вкладки 'Стойка' в модуле Объекты. @@ -28,6 +33,10 @@ class TestRackTab: Тесты покрывают следующие функциональные области: 1. test_rack_tab_content - Базовая структура и содержимое вкладки стойки 2. test_rack_tab_switching - Функциональность переключения между вкладками стойки + 3. test_rack_general_info_tab_fields - Заполнение полей вкладки 'Общая информация' + 4. test_rack_image_tab - Работа с вкладкой 'Изображение' + 5. test_rack_access_rules - Заполнение полей правил доступа + 6. test_rack_all_tabs_navigation - Навигация по всем вкладкам модального окна редактирования """ # Инициализируем атрибуты @@ -65,6 +74,8 @@ class TestRackTab: browser: Страница Playwright rack_name: Имя стойки для создания """ + logger.debug(f"Creating rack: {rack_name}") + # Нажимаем кнопку "Создать" на тулбаре self.location_page.click_create_button() @@ -93,6 +104,8 @@ class TestRackTab: # Нажимаем кнопку создания 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: """Удаляет стойку через контекстное меню. @@ -126,22 +139,7 @@ class TestRackTab: # Используем метод для удаления rack_edit.click_remove_button() - # 4. Проверяем уведомление об успешном удалении - требуется создать разработчику (заведена задача) - # Создаем экземпляр фрейма для доступа к alert компоненту - # create_child_frame = CreateChildElementFrame(browser) - - # Проверяем наличие любого alert-окна (не обязательно точного текста) - # create_child_frame.alert.check_alert_presence("") - - # Получаем текст alert, чтобы убедиться что удаление прошло успешно - # alert_text = create_child_frame.alert.get_text() - # logger.debug(f"Alert text after deletion: {alert_text}") - - # Проверяем что в тексте есть указание на успешное удаление - # assert "удален" in alert_text.lower() or "успешно" in alert_text.lower() - - # Закрываем alert - # create_child_frame.alert.close_alert() + logger.info(f"Rack '{rack_name}' deleted successfully") @pytest.fixture(scope="function", autouse=True) def setup(self, browser: Page) -> None: @@ -155,6 +153,7 @@ class TestRackTab: Args: browser (Page): Экземпляр страницы Playwright для взаимодействия с UI """ + # Авторизация в системе login_page = LoginPage(browser) login_page.do_login() @@ -175,11 +174,15 @@ class TestRackTab: 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) + logger.info("Test setup completed") + @pytest.fixture(scope="class", autouse=True) def cleanup_rack(self, browser: Page): """Фикстура для очистки созданной стойки после ВСЕХ тестов класса. @@ -190,6 +193,7 @@ class TestRackTab: Args: browser: Экземпляр страницы Playwright """ + # Тесты выполняются здесь yield @@ -220,6 +224,7 @@ class TestRackTab: self.main_page.click_subpanel_item("test-zone") self.main_page.wait_for_timeout(1000) + #@pytest.mark.develop def test_rack_tab_content(self, browser: Page) -> None: """Тест содержимого вкладки 'Стойка'. @@ -254,6 +259,9 @@ class TestRackTab: rt.wait_for_timeout(1000) + logger.info("test_rack_tab_content completed successfully") + + #@pytest.mark.develop def test_rack_tab_switching(self, browser: Page) -> None: """Тест переключения между вкладками стойки оборудования. @@ -273,10 +281,10 @@ class TestRackTab: rt.should_be_toolbar_buttons() rt.wait_for_timeout(1000) - @pytest.mark.develop + + #@pytest.mark.develop def test_rack_general_info_tab_fields(self, browser: Page) -> None: """Тест заполнения полей вкладки 'Общая информация' стойки.""" - logger = get_logger("RACK_GENERAL_INFO_TEST") rt = RackPage(browser) @@ -286,7 +294,7 @@ class TestRackTab: rt.wait_for_timeout(1000) # Создаем экземпляр ModalEditRack - rack_edit = ModalEditRack(browser, RACK_NAME) # ИЗМЕНЕНО: добавлен RACK_NAME + rack_edit = ModalEditRack(browser, RACK_NAME) # Создаем тестовые данные для заполнения всех полей rack_edit_data = RackEditData( @@ -306,13 +314,6 @@ class TestRackTab: # Checkbox поля ventilation_panel=True, - - # Правила доступа (если есть такие поля в форме) - #read_access_rules="admin" if "Правила доступа для чтения по умолчанию" in available_fields else "", - #write_access_rules="admin" if "Правила доступа для записи по умолчанию" in available_fields else "", - #sms_access_rules="admin" if "Правила доступа по умолчанию для получения СМС" in available_fields else "", - #email_access_rules="admin" if "Правила доступа по умолчанию для получения email сообщения" in available_fields else "", - #push_access_rules="admin" if "Правила доступа по умолчанию для получения push уведомлений" in available_fields else "", ) # Заполняем все поля формы @@ -326,9 +327,6 @@ class TestRackTab: rack_edit.click_done_button() rack_edit.wait_for_timeout(3000) - # Проверяем поля, которые мы заполнили, действительно заполнены - logger.info("=== Проверка, что все поля корректно заполнены ===") - # Вход в режим редактирования rt.click_edit_button() @@ -341,10 +339,181 @@ class TestRackTab: # Проверяем результаты assert verification_results["incorrectly_filled"] == 0, \ - f"Некорректно заполнены доступные поля: {verification_results['field_errors']}" + f"Available fields incorrectly filled: {verification_results['field_errors']}" assert verification_results["not_filled"] == 0, \ - f"Не заполнены доступные поля: {verification_results['field_errors']}" + f"Available fields not filled: {verification_results['field_errors']}" rack_edit.click_close_button() - logger.info("✓ General Info tab fields test completed") + + #@pytest.mark.develop + def test_rack_image_tab(self, browser: Page) -> None: + """Тест вкладки 'Изображение' стойки.""" + + rt = RackPage(browser) + + # Переходим в режим редактирования + rt.click_edit_button() + rt.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.info(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) + rt.wait_for_timeout(2000) + logger.info("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. + """ + + rt = RackPage(browser) + + # Переходим в режим редактирования + rt.click_edit_button() + rt.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" + ] + + # Заполняем поля - в КАЖДОЕ поле добавляются ВСЕ пользователи из custom_users + 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.info(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) + + # Возвращаемся в режим редактирования и проверяем снова + rt.click_edit_button() + rt.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() + + + #@pytest.mark.develop + def test_rack_all_tabs_navigation(self, browser: Page) -> None: + """Тест навигации по всем вкладкам модального окна редактирования стойки.""" + + rt = RackPage(browser) + + # Переходим в режим редактирования + rt.click_edit_button() + rt.wait_for_timeout(1000) + + # Создаем экземпляр ModalEditRack + rack_edit = ModalEditRack(browser, RACK_NAME) + + # Проверяем начальное состояние + initial_tab = rack_edit.get_active_tab() + assert initial_tab == ModalEditRack.TAB_GENERAL, \ + f"Initial tab should be '{ModalEditRack.TAB_GENERAL}', got '{initial_tab}'" + logger.debug(f"Initial tab: {initial_tab}") + + # Переключаемся на вкладку "Изображение" + rack_edit.switch_to_tab(ModalEditRack.TAB_IMAGE) + assert rack_edit.is_tab_active(ModalEditRack.TAB_IMAGE), \ + "Should be on Image tab after switching" + logger.debug(f"Switched to: {ModalEditRack.TAB_IMAGE}") + + # Переключаемся на вкладку "Настройки" + rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS) + assert rack_edit.is_tab_active(ModalEditRack.TAB_SETTINGS), \ + "Should be on Settings tab after switching" + logger.debug(f"Switched to: {ModalEditRack.TAB_SETTINGS}") + + # Возвращаемся на вкладку "Общая информация" + rack_edit.switch_to_tab(ModalEditRack.TAB_GENERAL) + assert rack_edit.is_tab_active(ModalEditRack.TAB_GENERAL), \ + "Should be back on General tab after switching" + logger.debug(f"Switched back to: {ModalEditRack.TAB_GENERAL}") + + # Закрываем окно редактирования + 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 0000000000000000000000000000000000000000..1e21bdcc5a2da2a4ae0d9cce24220e6f4f44fdf5 GIT binary patch literal 5340 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/ztp/test_expand_navigation_panel.py b/tests/e2e/ztp/test_expand_navigation_panel.py new file mode 100644 index 0000000..0ab6506 --- /dev/null +++ b/tests/e2e/ztp/test_expand_navigation_panel.py @@ -0,0 +1,168 @@ +"""Модуль тестов панели навигации. + +Содержит тесты для проверки функциональности +панели навигации в приложении. +""" + +import pytest +from playwright.sync_api import Page +from pages.main_page import MainPage +from pages.login_page import LoginPage + + +# @pytest.mark.smoke +class TestNavigationPanel: + """Класс тестов для проверки панели навигации. + + Тесты покрывают следующие сценарии: + - test_expand_panel: Проверяет полное раскрытие панели навигации + - test_sub_panel_item_click: Проверяет возможность клика заданного элемента в подпанели навигации + + Атрибуты: + browser: Фикстура для работы с браузером. + """ + + # @pytest.mark.develop + def test_expand_panel(self, browser: Page): + """Проверяет полное раскрытие панели навигации. + + Args: + browser: Фикстура для работы с браузером. + + """ + + # Действия: + lp = LoginPage(browser) + lp.do_login() + + # Мы на главной странице + mp = MainPage(browser) + + # Проверки: + # Проверяем наличие панели навигации + mp.should_be_navigation_panel() + + # Открываем все пункты панели + mp.click_main_navigation_panel_item("Настройки") + + mp.expand_navigation_subpanel() + + mp.click_main_navigation_panel_item("Объекты") + mp.wait_for_timeout(300) + + mp.expand_navigation_subpanel() + + # @pytest.mark.develop + def test_sub_panel_item_click(self, browser: Page): + """Проверяет возможность клика заданного элемента в подпанели навигации. + + Args: + browser: Фикстура для работы с браузером. + + """ + + # Действия: + lp = LoginPage(browser) + lp.do_login() + + # Мы на главной странице + mp = MainPage(browser) + + # Проверки: + # Проверяем наличие панели навигации + mp.should_be_navigation_panel() + + # Открываем разные пункты панели + mp.click_main_navigation_panel_item("Настройки") + + mp.click_subpanel_item("Обслуживание и диагностика") + mp.click_subpanel_item("Статус компонентов") + mp.wait_for_timeout(2000) + + mp.click_subpanel_item("Сеансы") + mp.click_subpanel_item("Настройки", parent="Сеансы") + + # Открываем/закрываем пункт панели + mp.click_subpanel_item("Пользователи") + mp.click_subpanel_item("Пользователи") + mp.wait_for_timeout(2000) + + # Открываем пункты панели с одинаковыми имнами, но разным расположением + mp.click_subpanel_item("Редактор") + mp.click_subpanel_item("Шаблоны") + mp.wait_for_timeout(2000) + + mp.click_subpanel_item("Zero Touch Provisioning") + # unsupported + # mp.click_subpanel_item("Шаблоны", parent="Zero Touch Provisioning") + mp.wait_for_timeout(2000) + + # Переходим к Объектам + mp.click_main_navigation_panel_item("Объекты") + mp.wait_for_timeout(5000) + + + mp.click_subpanel_item("test-zone") + mp.wait_for_timeout(3000) + + # Переходим к Стойке + mp.click_subpanel_item("Test-Rack-01") + mp.wait_for_timeout(5000) + + # Переходим Здание ЦОД 4 + # mp.click_subpanel_item("Здание ЦОД 4") + # mp.wait_for_timeout(3000) + + # # Переходим к Стойка КСПД с указанием родителя + # mp.click_subpanel_item("Стойка КСПД", parent="Здание ЦОД 4") + # mp.wait_for_timeout(5000) + + # Переходим к Объектам + mp.click_main_navigation_panel_item("Объекты") + mp.click_main_navigation_panel_item("Объекты") # баг + mp.wait_for_timeout(5000) + + # mp.click_subpanel_item("Виртуальные устройства") + # mp.wait_for_timeout(3000) + + # # Переходим к Стойка систем питания с указанием родителя + # mp.click_subpanel_item("Стойка систем питания", parent="Виртуальные устройства") + # mp.wait_for_timeout(5000) + + @pytest.mark.develop + def test_check_sub_panel_item_state(self, browser: Page): + """Проверяет наличие индикатора состояния заданного элемента в подпанели навигации. + + Args: + browser: Фикстура для работы с браузером. + + """ + + # Действия: + lp = LoginPage(browser) + lp.do_login() + + # Мы на главной странице + mp = MainPage(browser) + + # Проверки: + # Проверяем наличие панели навигации + mp.should_be_navigation_panel() + + # Открываем разные пункты панели + mp.click_main_navigation_panel_item("Настройки") + + mp.click_subpanel_item("Обслуживание и диагностика") + state_color = mp.check_subpanel_item_state("Сеансы") + assert not state_color, "Got state color but subpanel item 'Сеансы' has no state indicator" + mp.wait_for_timeout(2000) + + # Переходим к Объектам + mp.click_main_navigation_panel_item("Объекты") + mp.wait_for_timeout(5000) + + state_color = mp.check_subpanel_item_state("test-zone") + assert state_color, "State indicator has not found for subpanel item 'test-zone'" + + state_color = mp.check_subpanel_item_state("Test-Rack-01") + assert state_color, "State indicator has not found for subpanel item 'Test-Rack-01'" diff --git a/tests/e2e/ztp/test_ztp_templates_tab.py b/tests/e2e/ztp/test_ztp_templates_tab.py index 2c5f5f7..a08da0c 100644 --- a/tests/e2e/ztp/test_ztp_templates_tab.py +++ b/tests/e2e/ztp/test_ztp_templates_tab.py @@ -10,9 +10,7 @@ from pages.login_page import LoginPage from pages.main_page import MainPage from pages.ztp_templates_tab import ZTPTemplatesTab -pytest.skip("Пропуск всех тестов в этом файле в связи исключением данной функциональности", allow_module_level=True) -# @pytest.mark.smoke class TestZTPTemplatesTab: """Набор тестов для вкладки 'Шаблоны' в модуле Zero Touch Provisioning. @@ -23,10 +21,9 @@ class TestZTPTemplatesTab: 1. test_templates_tab_content - Базовая структура и содержимое вкладки 2. test_templates_table_row_highlighting - Визуальное выделение строк таблицы 3. test_templates_table_scrolling - Навигация по таблице с большим объемом данных - 4. test_templates_modal_window_close_buttons - Закрытие модальных окон разными способами - 5. test_templates_modal_window_content - Структура и содержимое модальных окон - 6. test_templates_modal_window_scrolling - Навигация в модальных окнах - 7. test_templates_modal_window_api_data_consistency - Синхронизация данных UI и API + 4. test_templates_modal_window_content - Структура и содержимое модальных окон + 5. test_templates_modal_window_scrolling - Навигация в модальных окнах + 6. test_templates_modal_window_api_data_consistency - Синхронизация данных UI и API Фикстура setup обеспечивает подготовку тестового окружения: - Авторизацию в системе @@ -56,7 +53,7 @@ class TestZTPTemplatesTab: main_page.click_subpanel_item("Шаблоны", parent="Zero Touch Provisioning") main_page.wait_for_timeout(5000) - # @pytest.mark.develop + def test_templates_tab_content(self, browser: Page) -> None: """Тест содержимого вкладки 'Шаблоны'. @@ -77,7 +74,7 @@ class TestZTPTemplatesTab: browser.wait_for_timeout(5000) # Проверка содержимого таблицы шаблонов - ztp_templates_tab.check_ztp_templates_table_content() + ztp_templates_tab.check_templates_table_content() def test_templates_table_row_highlighting(self, browser: Page) -> None: """Проверка выделения строк в таблице шаблонов. @@ -144,57 +141,6 @@ class TestZTPTemplatesTab: # Проверка видимости первой строки ztp_templates_tab.check_templates_table_first_row_visibility() - def test_templates_modal_window_close_buttons(self, browser: Page) -> None: - """Тест закрытия модального окна шаблона разными способами. - - Проверяет: - 1. Закрытие модального окна через кнопку 'Закрыть' в содержимом окна - 2. Закрытие модального окна через кнопку закрытия в тулбаре - 3. Корректность закрытия окна в обоих случаях - - Args: - browser (Page): Экземпляр страницы Playwright для взаимодействия с UI. - """ - # Инициализация страницы шаблонов - ztp_templates_tab = ZTPTemplatesTab(browser) - - # Проверка наличия таблицы шаблонов - ztp_templates_tab.should_be_templates_table() - - # Добавляем задержку для загрузки данных - browser.wait_for_timeout(2000) - - # Тест 1: Закрытие через кнопку 'Закрыть' в содержимом окна - print("Тест 1: Закрытие через кнопку 'Закрыть'") - title = ztp_templates_tab.open_template_modal_by_index(0) - browser.wait_for_timeout(1000) - - # Проверка открытия модального окна - ztp_templates_tab.should_be_modal_window() - - # Закрытие через кнопку 'Закрыть' - ztp_templates_tab.close_modal_window(title) - - # Проверяем, что модальное окно закрылось - ztp_templates_tab.should_not_be_modal_window() - browser.wait_for_timeout(1000) - - # Тест 2: Закрытие через кнопку в тулбаре - print("Тест 2: Закрытие через кнопку в тулбаре") - title = ztp_templates_tab.open_template_modal_by_index(0) - browser.wait_for_timeout(1000) - - # Проверка открытия модального окна - ztp_templates_tab.should_be_modal_window() - - # Закрытие через кнопку в тулбаре - ztp_templates_tab.close_modal_window_by_toolbar_button(title) - - # Проверяем, что модальное окно закрылось - ztp_templates_tab.should_not_be_modal_window() - - print("Оба способа закрытия модального окна работают корректно") - def test_templates_modal_window_content(self, browser: Page) -> None: """Тест содержимого модального окна шаблона. @@ -215,7 +161,7 @@ class TestZTPTemplatesTab: browser.wait_for_timeout(2000) # Открываем модальное окно, кликая на первую строку таблицы - title = ztp_templates_tab.open_template_modal_by_index(0) + template_name = ztp_templates_tab.open_template_modal_by_index(0) # Добавляем задержку для открытия модального окна browser.wait_for_timeout(1000) @@ -224,14 +170,15 @@ class TestZTPTemplatesTab: ztp_templates_tab.should_be_modal_window() # Проверка содержимого модального окна - ztp_templates_tab.check_ztp_templates_modal_content(title) + ztp_templates_tab.check_templates_modal_content(template_name) - # Закрытие модального окна через кнопку 'Закрыть' - ztp_templates_tab.close_modal_window(title) + # Закрытие модального окна через кнопку закрытия + ztp_templates_tab.close_modal_window_by_toolbar_button(template_name) # Проверяем, что модальное окно закрылось ztp_templates_tab.should_not_be_modal_window() + #@pytest.mark.skip(reason=" Временно исключено из тестирования") def test_templates_modal_window_scrolling(self, browser: Page) -> None: """Тест скроллинга модального окна шаблона. @@ -251,7 +198,7 @@ class TestZTPTemplatesTab: browser.wait_for_timeout(2000) # Открываем модальное окно, кликая на первую строку таблицы - title = ztp_templates_tab.open_template_modal_by_index(0) + template_name = ztp_templates_tab.open_template_modal_by_index(0) # Добавляем задержку для открытия модального окна browser.wait_for_timeout(1000) @@ -265,22 +212,23 @@ class TestZTPTemplatesTab: if is_scrollable: print("Модальное окно поддерживает вертикальный скроллинг") - # Прокрутка вниз модального окна + # Прокрутка вниз ztp_templates_tab.scroll_modal_down() browser.wait_for_timeout(1000) - # Прокрутка вверх модального окна + # Прокрутка вверх ztp_templates_tab.scroll_modal_up() browser.wait_for_timeout(1000) else: print("Модальное окно не поддерживает вертикальный скроллинг") - # Закрытие модального окна через кнопку 'Закрыть' - ztp_templates_tab.close_modal_window(title) + # Закрытие модального окна через кнопку закрытия + ztp_templates_tab.close_modal_window_by_toolbar_button(template_name) # Проверяем, что модальное окно закрылось ztp_templates_tab.should_not_be_modal_window() + #@pytest.mark.skip(reason=" Временно исключено из тестирования") def test_templates_modal_window_api_data_consistency(self, browser: Page) -> None: """Тест соответствия данных модального окна шаблона данным из API. @@ -304,7 +252,7 @@ class TestZTPTemplatesTab: browser.wait_for_timeout(5000) # Открываем модальное окно, кликая на первую строку таблицы и возвращаем имя заголовка - title = ztp_templates_tab.open_template_modal_by_index(0) + template_name = ztp_templates_tab.open_template_modal_by_index(0) # Добавляем задержку для открытия модального окна browser.wait_for_timeout(2000) @@ -313,10 +261,10 @@ class TestZTPTemplatesTab: ztp_templates_tab.should_be_modal_window() # Проверка соответствия данных модального окна данным из API - ztp_templates_tab.verify_template_data_with_api(title) + ztp_templates_tab.verify_template_data_with_api(template_name) # Закрытие модального окна через кнопку закрытия - ztp_templates_tab.close_modal_window_by_toolbar_button(title) + ztp_templates_tab.close_modal_window_by_toolbar_button(template_name) # Проверяем, что модальное окно закрылось ztp_templates_tab.should_not_be_modal_window()