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 a8c3763..0000000 Binary files a/components_derived/accounting_objects/__pycache__/rack_maker.cpython-313.pyc and /dev/null differ diff --git a/components_derived/frames/__pycache__/create_child_element_frame.cpython-313.pyc b/components_derived/frames/__pycache__/create_child_element_frame.cpython-313.pyc deleted file mode 100644 index 5e901e7..0000000 Binary files a/components_derived/frames/__pycache__/create_child_element_frame.cpython-313.pyc and /dev/null differ diff --git a/components_derived/modal_edit_rack.py b/components_derived/modal_edit_rack.py new file mode 100644 index 0000000..00d3509 --- /dev/null +++ b/components_derived/modal_edit_rack.py @@ -0,0 +1,1401 @@ +"""Модуль для работы с модальным окном редактирования стойки.""" + +import re +from dataclasses import dataclass +from typing import Optional, List, Tuple, Any +from playwright.sync_api import Page +from tools.logger import get_logger +from locators.rack_locators import RackLocators +from elements.text_input_element import TextInput +from elements.text_element import Text +from elements.checkbox_element import Checkbox +from components.modal_window_component import ModalWindowComponent +from components.dropdown_list_component import DropdownList +from components.confirm_component import ConfirmComponent + + +logger = get_logger("MODAL_EDIT_RACK") +logger.setLevel("INFO") + +@dataclass +class RackEditData: + """Класс для хранения данных редактирования стойки. + + Содержит все возможные поля, которые могут быть изменены + в модальном окне редактирования стойки. + """ + + # Основные поля (редактируемые) + name: str = "" + serial: str = "" + inventory: str = "" + comment: str = "" + allocated_power: str = "" + + # Combobox поля (редактируемые) + cable_entry: str = "" + state: str = "" + depth: str = "" + usize: str = "" + owner: str = "" + service_org: str = "" + project: str = "" + + # Checkbox поля (редактируемые) + ventilation_panel: Optional[bool] = None + + # Правила доступа + read_access_rules: str = "" + write_access_rules: str = "" + sms_access_rules: str = "" + email_access_rules: str = "" + push_access_rules: str = "" + + +class ModalEditRack(ModalWindowComponent): + """Компонент для работы с модальным окном редактирования стойки. + + Предоставляет методы для взаимодействия с элементами окна: + - переключение между вкладками + - заполнение полей общей информации + - работа с изображениями + - настройка правил доступа + - сохранение/отмена изменений + - удаление стойки + """ + + # Константы для названий вкладок + TAB_GENERAL = "Общая информация" + TAB_IMAGE = "Изображение" + TAB_SETTINGS = "Настройки" + + # Маппинг полей для заполнения текстовых полей + TEXT_FIELDS_MAPPING = { + "Имя": ("name", "name_input"), + "Комментарий": ("comment", "comment_input"), + "Серийный номер": ("serial", "serial_input"), + "Инвентарный номер": ("inventory", "inventory_input"), + "Выделенная мощность (Вт/ВА)": ("allocated_power", "power_input"), + } + + # Маппинг полей для заполнения combobox полей + COMBOBOX_FIELDS_MAPPING = { + "Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"), + "Состояние": ("state", "state_input", "state_list"), + "Глубина (мм)": ("depth", "depth_input", "depth_list"), + "Высота в юнитах": ("usize", "usize_input", "usize_list"), + "Владелец": ("owner", "owner_input", "owner_list"), + "Обслуживающая организация": ("service_org", "service_input", "service_list"), + "Проект/Титул": ("project", "project_input", "project_list") + } + + # Локаторы для текстовых полей (из RackLocators) + TEXT_FIELDS_LOCATORS = { + "Имя": RackLocators.INPUT_FORM_RACK_DATA_FIELD_NAME, + "Комментарий": RackLocators.INPUT_FORM_RACK_DATA_FIELD_COMMENT, + "Серийный номер": RackLocators.INPUT_FORM_RACK_DATA_FIELD_SERIAL, + "Инвентарный номер": RackLocators.INPUT_FORM_RACK_DATA_FIELD_INVENTORY, + "Выделенная мощность (Вт/ВА)": RackLocators.INPUT_FORM_RACK_DATA_FIELD_POWER, + } + + # Локаторы для combobox полей (из RackLocators) + COMBOBOX_FIELDS_LOCATORS = { + "Ввод кабеля": RackLocators.INPUT_FORM_RACK_DATA_FIELD_CABLE_ENTRY, + "Состояние": RackLocators.INPUT_FORM_RACK_DATA_FIELD_CONDITION_TYPE, + "Глубина (мм)": RackLocators.INPUT_FORM_RACK_DATA_FIELD_DEPTH, + "Высота в юнитах": RackLocators.INPUT_FORM_RACK_DATA_FIELD_USIZE, + "Владелец": RackLocators.INPUT_FORM_RACK_DATA_FIELD_OWNER, + "Обслуживающая организация": RackLocators.INPUT_FORM_RACK_DATA_FIELD_SERVICE_PROVIDER, + "Проект/Титул": RackLocators.INPUT_FORM_RACK_DATA_FIELD_PROJECT, + } + + # Маппинг полей для вкладки "Настройки" + ACCESS_RULES_MAPPING = { + "Правила доступа для чтения": ( + "read_access_rules", "rules_read_input", "rules_read_list" + ), + "Правила доступа для записи": ( + "write_access_rules", "rules_write_input", "rules_write_list" + ), + "Правила доступа для получения СМС": ( + "sms_access_rules", "rules_sms_input", "rules_sms_list" + ), + "Правила доступа для получения email сообщения": ( + "email_access_rules", "rules_email_input", "rules_email_list" + ), + "Правила доступа для получения push уведомлений": ( + "push_access_rules", "rules_push_input", "rules_push_list" + ), + } + + # Локаторы для полей правил доступа (из RackLocators) + ACCESS_RULES_LOCATORS = { + "Правила доступа для чтения": RackLocators.SETTINGS_READ_RULES, + "Правила доступа для записи": RackLocators.SETTINGS_WRITE_RULES, + "Правила доступа для получения СМС": RackLocators.SETTINGS_SMS_RULES, + "Правила доступа для получения email сообщения": RackLocators.SETTINGS_EMAIL_RULES, + "Правила доступа для получения push уведомлений": RackLocators.SETTINGS_PUSH_RULES, + } + + def __init__(self, page: Page, rack_name: str) -> None: + """Инициализирует компонент редактирования стойки. + + Args: + page: Экземпляр страницы Playwright. + rack_name: Имя редактируемой стойки. + """ + + super().__init__(page) + self.rack_name = rack_name + self.page = page + self.available_fields = None + self.active_tab = self.TAB_GENERAL + self.tabs = {} + self.content_items = {} + self.delete_confirm = None + + # Настройка заголовка и кнопки закрытия + self.window_title = rack_name + locator_button_toolbar_close = ( + self.page.get_by_role("navigation") + .filter(has_text=re.compile(self.window_title)) + .get_by_role("button") + ) + + self.add_toolbar_title(self.window_title) + self.add_toolbar_button(locator_button_toolbar_close, "close") + + # Инициализация компонента подтверждения удаления + self.delete_confirm = ConfirmComponent(page, " Отмена ", " Удалить ") + + # Инициализация вкладок и содержимого + self._init_tabs() + self._init_active_tab_content() + self._init_toolbar_buttons() + + def _init_tabs(self) -> None: + """Инициализирует вкладки окна редактирования.""" + + self.tabs = { + self.TAB_GENERAL: self.page.locator(RackLocators.MODAL_TAB_GENERAL), + self.TAB_IMAGE: self.page.locator(RackLocators.MODAL_TAB_IMAGE), + self.TAB_SETTINGS: self.page.locator(RackLocators.MODAL_TAB_SETTINGS), + } + logger.debug(f"Initialized tabs: {list(self.tabs.keys())}") + + def _init_active_tab_content(self) -> None: + """Инициализирует содержимое активной вкладки.""" + + self.content_items = {} + + if self.active_tab == self.TAB_GENERAL: + self._init_general_tab_content() + elif self.active_tab == self.TAB_IMAGE: + self._init_image_tab_content() + else: + self._init_settings_tab_content() + + def _init_general_tab_content(self) -> None: + """Инициализирует содержимое вкладки 'Общая информация'.""" + + # Получаем доступные поля формы с помощью базового метода + self.available_fields = self.get_input_fields_locators( + self.page.locator(RackLocators.INPUT_FORM_RACK_DATA)) + + self._init_text_fields() + self._init_combobox_fields() + self._init_checkbox_fields() + + def _init_text_fields(self) -> None: + """Инициализирует текстовые поля формы.""" + + for field_label, (_, widget_name) in self.TEXT_FIELDS_MAPPING.items(): + locator = self.TEXT_FIELDS_LOCATORS.get(field_label) + if not locator: + continue + + self._init_single_text_field(field_label, locator, widget_name) + + def _init_single_text_field(self, field_label: str, locator: str, widget_name: str) -> None: + """Инициализирует одно текстовое поле. + + Args: + field_label: Метка поля. + locator: Локатор поля. + widget_name: Имя виджета. + """ + + try: + element = self.page.locator(locator).first + if element.count() > 0 and element.is_visible(): + field_input = TextInput(self.page, element, widget_name) + self.add_content_item(widget_name, field_input) + logger.debug(f"Initialized text field: '{field_label}'") + except Exception as e: + logger.error(f"Error initializing text field '{field_label}': {e}") + + def _init_combobox_fields(self) -> None: + """Инициализирует combobox поля формы.""" + + for field_label, (_, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items(): + locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label) + if not locator: + continue + + self._init_single_combobox_field(field_label, locator, input_name, list_name) + + def _init_single_combobox_field( + self, field_label: str, locator: str, input_name: str, list_name: str + ) -> None: + """Инициализирует одно combobox поле. + + Args: + field_label: Метка поля. + locator: Локатор поля. + input_name: Имя поля ввода. + list_name: Имя списка. + """ + + try: + element = self.page.locator(locator).first + if element.count() > 0 and element.is_visible(): + field_input = TextInput(self.page, element, input_name) + self.add_content_item(input_name, field_input) + self.add_content_item(list_name, DropdownList(self.page)) + logger.debug(f"Initialized combobox field: '{field_label}'") + except Exception as e: + logger.error(f"Error initializing combobox field '{field_label}': {e}") + + def _init_checkbox_fields(self) -> None: + """Инициализирует checkbox поля формы.""" + + try: + self._init_ventilation_checkbox() + except Exception as e: + logger.error(f"Error initializing checkbox: {e}") + + def _init_ventilation_checkbox(self) -> None: + """Инициализирует чекбокс вентиляционной панели.""" + + checkbox_input = self.page.locator( + RackLocators.INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION + ).first + + if checkbox_input.count() == 0: + return + + checkbox = Checkbox(self.page, checkbox_input, "ventilation_panel") + self.add_content_item("ventilation_checkbox", checkbox) + + label_locator = self.page.locator("label:has-text('Вентиляционная панель')").first + if label_locator.count() > 0: + label_text = Text(self.page, label_locator, "ventilation_checkbox_label") + self.add_content_item("ventilation_checkbox_label", label_text) + + logger.debug("Initialized ventilation panel checkbox") + + def _init_image_tab_content(self) -> None: + """Инициализирует содержимое вкладки 'Изображение'.""" + + try: + self._init_image_upload_elements() + logger.debug("Image tab content initialized") + except Exception as e: + logger.error(f"Error initializing image tab content: {e}") + + def _init_image_upload_elements(self) -> None: + """Инициализирует элементы загрузки изображения.""" + + image_tab_container = self.page.locator(RackLocators.IMAGE_UPLOAD_CONTAINER) + upload_icon = image_tab_container.locator(RackLocators.IMAGE_UPLOAD_ICON) + self.add_content_item("image_upload_icon", upload_icon) + + upload_input = image_tab_container.locator(RackLocators.IMAGE_UPLOAD_INPUT) + self.add_content_item("image_upload_input", upload_input) + + def _init_settings_tab_content(self) -> None: + """Инициализирует содержимое вкладки 'Настройки доступа'.""" + + self._init_access_rules_fields() + logger.debug("Settings tab content initialized") + + def _init_access_rules_fields(self) -> None: + """Инициализирует поля правил доступа.""" + + settings_container = self.page.locator(RackLocators.SETTINGS_CONTAINER) + + # Используем базовый метод для получения всех полей + fields_locators = self.get_input_fields_locators(settings_container) + + # Для каждого поля из маппинга проверяем наличие и инициализируем + for field_label, (_, input_name, list_name) in self.ACCESS_RULES_MAPPING.items(): + if field_label not in fields_locators: + continue + + self._init_single_access_rule_field( + field_label, fields_locators[field_label], input_name, list_name + ) + + logger.debug( + f"Settings tab content initialized. Found fields: {list(fields_locators.keys())}" + ) + + def _init_single_access_rule_field( + self, field_label: str, input_container: Any, input_name: str, list_name: str + ) -> None: + """Инициализирует одно поле правила доступа. + + Args: + field_label: Метка поля. + input_container: Контейнер поля ввода. + input_name: Имя поля ввода. + list_name: Имя списка. + """ + + try: + # Ищем input внутри контейнера + input_element = input_container.locator("input, textarea, select").first + if input_element.count() > 0: + field_input = TextInput(self.page, input_element, input_name) + self.add_content_item(input_name, field_input) + self.add_content_item(list_name, DropdownList(self.page)) + logger.debug(f"Initialized access rule field: '{field_label}'") + except Exception as e: + logger.error(f"Error initializing access rule field '{field_label}': {e}") + + def _init_toolbar_buttons(self) -> None: + """Инициализирует кнопки тулбара.""" + + self.add_button(self.page.locator(RackLocators.TOOLBAR_REPLACE_BUTTON), "replace") + self.add_button(self.page.locator(RackLocators.TOOLBAR_DONE_BUTTON), "save") + self.add_button(self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON), "cancel") + self.add_button(self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON), "delete") + + # Действия с вкладками + def switch_to_tab(self, tab_name: str) -> None: + """Переключается на указанную вкладку. + + Args: + tab_name: Название вкладки для переключения. + + Raises: + ValueError: Если указана неизвестная вкладка. + """ + + if tab_name not in self.tabs: + raise ValueError( + f"Unknown tab: {tab_name}. Available tabs: {list(self.tabs.keys())}" + ) + + self.tabs[tab_name].click() + self.wait_for_timeout(1000) + self.active_tab = tab_name + self._init_active_tab_content() + logger.info(f"Switched to tab: {tab_name}") + + def get_active_tab(self) -> str: + """Возвращает название активной вкладки. + + Returns: + Название активной вкладки. + """ + return self.active_tab + + def is_tab_active(self, tab_name: str) -> bool: + """Проверяет, активна ли указанная вкладка. + + Args: + tab_name: Название вкладки для проверки. + + Returns: + True если вкладка активна, иначе False. + """ + return self.active_tab == tab_name + + # Действия с изображениями + def upload_image(self, image_path: str) -> None: + """Загружает изображение на вкладке 'Изображение'. + + Args: + image_path: Путь к файлу изображения. + + Raises: + RuntimeError: Если не найден элемент для загрузки изображения. + """ + + if self.active_tab != self.TAB_IMAGE: + self.switch_to_tab(self.TAB_IMAGE) + + try: + self._perform_image_upload(image_path) + except Exception as e: + logger.error(f"Error uploading image: {e}") + raise + + def _perform_image_upload(self, image_path: str) -> None: + """Выполняет загрузку изображения. + + Args: + image_path: Путь к файлу изображения. + + Raises: + RuntimeError: Если не найден элемент для загрузки изображения. + """ + + upload_icon = self.get_content_item("image_upload_icon") + if upload_icon and upload_icon.count() > 0: + upload_icon.click() + self.wait_for_timeout(500) + + upload_input = self.get_content_item("image_upload_input") + if upload_input and upload_input.count() > 0: + upload_input.set_input_files(image_path) + logger.info(f"Uploaded image: {image_path}") + return + + # Пробуем найти input напрямую + file_input = self.page.locator(RackLocators.IMAGE_UPLOAD_INPUT) + if file_input.count() > 0: + file_input.first.set_input_files(image_path) + logger.info(f"Uploaded image via page input: {image_path}") + return + + raise RuntimeError("Image upload input not found") + + def has_current_image(self) -> bool: + """Проверяет, есть ли текущее изображение у стойки. + + Returns: + True если изображение загружено, иначе False. + """ + + if self.active_tab != self.TAB_IMAGE: + self.switch_to_tab(self.TAB_IMAGE) + + try: + return self._check_image_exists() + except Exception as e: + logger.error(f"Error checking for current image: {e}") + return False + + def _check_image_exists(self) -> bool: + """Проверяет наличие изображения. + + Returns: + True если изображение существует, иначе False. + """ + + image_container = self.page.locator(RackLocators.IMAGE_UPLOAD_CONTAINER) + img_element = image_container.locator(RackLocators.IMAGE_PREVIEW) + upload_icon = image_container.locator(RackLocators.IMAGE_UPLOAD_ICON) + + if img_element.count() > 0 and img_element.first.is_visible(): + return True + + return False + + # Действия с настройками доступа + def fill_access_rules( + self, + users_to_add: Optional[List[str]] = None, + target_fields: Optional[List[str]] = None + ) -> dict: + """Заполняет правила доступа в указанных полях. + + В каждый combobox добавляются указанные пользователи. + + Args: + users_to_add: Список пользователей для добавления. + target_fields: Список целевых полей для заполнения. + + Returns: + Словарь с результатами заполнения: + - access_rules_filled: количество добавленных пользователей + - errors: список ошибок + - fields_processed: обработанные поля + - field_stats: статистика по каждому полю + """ + + if self.active_tab != self.TAB_SETTINGS: + self.switch_to_tab(self.TAB_SETTINGS) + + results = self._init_fill_results() + + if users_to_add is None: + users_to_add = self._get_default_users() + + fields_to_process = self._get_fields_to_process(target_fields) + + logger.info(f"Processing fields: {[f[0] for f in fields_to_process]}") + logger.info(f"Users to add: {users_to_add}") + + for field_index, (field_label, _, input_name, _) in enumerate(fields_to_process): + self._process_single_field( + field_index, field_label, users_to_add, results + ) + + self._log_fill_summary(results, len(fields_to_process), len(users_to_add)) + + return results + + def _init_fill_results(self) -> dict: + """Инициализирует словарь результатов заполнения. + + Returns: + Словарь с начальными значениями результатов. + """ + + return { + "access_rules_filled": 0, + "errors": [], + "fields_processed": [], + "field_stats": {} + } + + def _get_default_users(self) -> List[str]: + """Возвращает список пользователей по умолчанию. + + Returns: + Список пользователей. + """ + + return [ + "TestUserRulesAdmin", + "TestUserRulesOper", + "TestUserRulesManager", + "TestUserRulesSec", + "TestUserRulesCollector" + ] + + def _process_single_field( + self, + field_index: int, + field_label: str, + users_to_add: List[str], + results: dict + ) -> None: + """Обрабатывает одно поле правил доступа. + + Args: + field_index: Индекс поля. + field_label: Название поля. + users_to_add: Список пользователей для добавления. + results: Словарь с результатами. + """ + + # Инициализируем статистику для поля + results["field_stats"][field_label] = { + "expected": len(users_to_add), + "added": 0, + "failed_users": [] + } + + field_locator = self.ACCESS_RULES_LOCATORS.get(field_label) + if not field_locator: + results["errors"].append(f"Locator not found for field '{field_label}'") + return + + try: + if field_index > 0: + self.page.mouse.click(10, 10) + self.wait_for_timeout(500) + + results["fields_processed"].append(field_label) + + # Находим элемент поля + field_element = self.page.locator(field_locator).first + if field_element.count() == 0: + results["errors"].append(f"Field element not found for '{field_label}'") + return + + self._clear_field(field_element) + + # Открываем combobox и добавляем пользователей + added_count, field_errors = self._open_dropdown_and_add_users( + field_element, field_label, users_to_add + ) + + results["access_rules_filled"] += added_count + results["field_stats"][field_label]["added"] = added_count + results["field_stats"][field_label]["failed_users"] = field_errors + results["errors"].extend([f"{field_label}: {error}" for error in field_errors]) + + # Закрываем dropdown + self.page.mouse.click(10, 10) + self.wait_for_timeout(500) + + logger.info(f"Field '{field_label}': added {added_count}/{len(users_to_add)} users") + + except Exception as e: + results["errors"].append(f"Error processing field {field_label}: {str(e)}") + + def _get_fields_to_process( + self, + target_fields: Optional[List[str]] = None + ) -> List[Tuple[str, str, str, str]]: + """Определяет поля для обработки. + + Args: + target_fields: Список целевых полей. + + Returns: + Список кортежей (field_label, attr_name, input_name, list_name). + """ + + if target_fields is None: + return list(self.ACCESS_RULES_MAPPING.items()) + + fields_to_process = [] + for field_attr in target_fields: + for field_label, (attr_name, input_name, list_name) in self.ACCESS_RULES_MAPPING.items(): + if attr_name == field_attr: + fields_to_process.append((field_label, attr_name, input_name, list_name)) + break + return fields_to_process + + def _clear_field(self, field_element: Any) -> None: + """Очищает поле от выбранных значений. + + Args: + field_element: Элемент поля для очистки. + """ + + parent_container = field_element.locator( + "xpath=ancestor::div[contains(@class, 'v-input')]" + ).first + + if parent_container.count() == 0: + return + + clear_button = parent_container.locator( + ".v-input__icon--clear button, .v-input__icon--append button, i.mdi-close-circle" + ).first + + if clear_button.count() > 0 and clear_button.is_visible(): + clear_button.click() + self.wait_for_timeout(500) + + def _open_dropdown_and_add_users( + self, + field_element: Any, + field_label: str, + users_to_add: List[str] + ) -> Tuple[int, List[str]]: + """Открывает выпадающий список и добавляет пользователей. + + Args: + field_element: Элемент поля. + field_label: Название поля. + users_to_add: Список пользователей для добавления. + + Returns: + Кортеж (количество добавленных, список ошибок). + """ + + added_count = 0 + errors = [] + + field_element.click(force=True) + self.wait_for_timeout(1500) + + dropdown_menu = self._get_dropdown_menu() + if not dropdown_menu: + errors.append(f"Could not open dropdown for {field_label}") + return added_count, errors + + for username in users_to_add: + added, error = self._add_user_to_dropdown(dropdown_menu, username, field_label) + if added: + added_count += 1 + if error: + errors.append(error) + + return added_count, errors + + def _get_dropdown_menu(self) -> Any: + """Возвращает выпадающее меню. + + Returns: + Locator выпадающего меню или None, если меню не найдено. + """ + + dropdown_menu = self.page.locator(RackLocators.MENU_ACTIVE_RACK_FORM).first + if dropdown_menu.count() == 0 or not dropdown_menu.is_visible(): + dropdown_menu = self.page.locator( + ".v-menu__content--active, .menuable__content__active" + ).first + + if dropdown_menu.count() == 0 or not dropdown_menu.is_visible(): + return None + return dropdown_menu + + def _add_user_to_dropdown( + self, + dropdown_menu: Any, + username: str, + field_label: str + ) -> Tuple[bool, Optional[str]]: + """Добавляет пользователя из выпадающего списка. + + Args: + dropdown_menu: Выпадающее меню. + username: Имя пользователя. + field_label: Название поля. + + Returns: + Кортеж (добавлен ли пользователь, сообщение об ошибке или None). + """ + + try: + user_item = dropdown_menu.locator(f"[role='listitem']:has-text('{username}')").first + if user_item.count() == 0: + user_item = dropdown_menu.locator(f"div:has-text('{username}')").first + + if user_item.count() > 0: + user_item.click() + self.wait_for_timeout(500) + return True, None + + return False, f"User '{username}' not found in dropdown for {field_label}" + except Exception as e: + return False, f"Failed to add user '{username}' to {field_label}: {str(e)}" + + def _log_fill_summary(self, results: dict, fields_count: int, users_count: int) -> None: + """Логирует итоговую статистику заполнения. + + Args: + results: Словарь с результатами. + fields_count: Количество полей. + users_count: Количество пользователей. + """ + + total_expected = fields_count * users_count + logger.info(f"Total added: {results['access_rules_filled']}/{total_expected}") + + for field_label, stats in results["field_stats"].items(): + if stats["added"] < stats["expected"]: + logger.warning( + f"Field '{field_label}' added only {stats['added']}/{stats['expected']} users. " + f"Failed: {stats['failed_users']}" + ) + + def verify_access_rules( + self, + expected_users: Optional[List[str]] = None, + target_fields: Optional[List[str]] = None + ) -> dict: + """Проверяет заполнение правил доступа в указанных полях. + + Проверяет, что в каждом поле есть все указанные пользователи. + + Args: + expected_users: Список ожидаемых пользователей. + target_fields: Список целевых полей для проверки. + + Returns: + Словарь с результатами проверки: + - total_expected_fields: общее количество ожидаемых значений + - correctly_filled: количество корректно заполненных + - incorrectly_filled: количество некорректно заполненных + - field_errors: список ошибок по полям + - fields_verified: список проверенных полей + - expected_users: список ожидаемых пользователей + """ + + if self.active_tab != self.TAB_SETTINGS: + self.switch_to_tab(self.TAB_SETTINGS) + + if expected_users is None: + expected_users = self._get_default_users() + + fields_to_verify = self._get_fields_to_process(target_fields) + total_expected_fields = len(fields_to_verify) * len(expected_users) + + results = { + "total_expected_fields": total_expected_fields, + "correctly_filled": 0, + "incorrectly_filled": 0, + "field_errors": [], + "fields_verified": [field_label for field_label, _, _, _ in fields_to_verify], + "expected_users": expected_users + } + + for field_label, _, _, _ in fields_to_verify: + self._verify_single_field(field_label, expected_users, results) + + if results["total_expected_fields"] > 0: + success_rate = results["correctly_filled"] / results["total_expected_fields"] * 100 + logger.info( + f"Access rules verification: {results['correctly_filled']}/" + f"{results['total_expected_fields']} ({success_rate:.1f}%)" + ) + + return results + + def _verify_single_field( + self, + field_label: str, + expected_users: List[str], + results: dict + ) -> None: + """Проверяет одно поле правил доступа. + + Args: + field_label: Название поля. + expected_users: Список ожидаемых пользователей. + results: Словарь с результатами для обновления. + """ + + field_locator = self.ACCESS_RULES_LOCATORS.get(field_label) + if not field_locator: + self._add_field_error( + results, field_label, expected_users, + f"Locator not found for field '{field_label}'" + ) + return + + try: + field_element = self.page.locator(field_locator).first + if field_element.count() == 0: + self._add_field_error( + results, field_label, expected_users, + f"Field '{field_label}' not found" + ) + return + + selected_users = self._get_selected_users(field_element) + logger.debug(f"Field '{field_label}' selected users: {selected_users}") + + for expected_user in expected_users: + if expected_user in selected_users: + results["correctly_filled"] += 1 + else: + results["incorrectly_filled"] += 1 + results["field_errors"].append( + f"Field '{field_label}' missing user: {expected_user} " + f"(selected: {selected_users})" + ) + + except Exception as e: + self._add_field_error( + results, field_label, expected_users, + f"Error verifying {field_label}: {str(e)}" + ) + logger.error(f"Error verifying {field_label}: {e}") + + def _get_selected_users(self, field_element: Any) -> List[str]: + """Получает список выбранных пользователей из поля. + + Args: + field_element: Элемент поля. + + Returns: + Список выбранных пользователей. + """ + + selected_users = [] + parent_container = field_element.locator( + "xpath=ancestor::div[contains(@class, 'v-input')]" + ).first + + if parent_container.count() == 0: + return selected_users + + selections_container = parent_container.locator( + ".v-select__selections, .v-chip__content" + ).first + + if selections_container.count() == 0: + return selected_users + + user_elements = selections_container.locator( + "span.v-chip__content, span.v-chip, span:not([class])" + ).all() + + for element in user_elements: + user_text = self._extract_user_text(element) + if user_text: + selected_users.append(user_text) + + return selected_users + + def _extract_user_text(self, element: Any) -> Optional[str]: + """Извлекает текст пользователя из элемента. + + Args: + element: Элемент DOM. + + Returns: + Текст пользователя или None. + """ + + user_text = element.text_content() or "" + user_text = user_text.strip() + + if not user_text or user_text in [",", " ", ""]: + return None + + if user_text.startswith(','): + user_text = user_text[1:].strip() + + return user_text if user_text else None + + def _add_field_error( + self, + results: dict, + field_label: str, + expected_users: List[str], + error_msg: str + ) -> None: + """Добавляет ошибку для поля. + + Args: + results: Словарь с результатами. + field_label: Название поля (не используется, оставлен для единообразия). + expected_users: Список ожидаемых пользователей. + error_msg: Сообщение об ошибке. + """ + + for _ in expected_users: + results["incorrectly_filled"] += 1 + results["field_errors"].append(error_msg) + + # Действия с кнопками + def click_close_button(self) -> None: + """Закрывает окно через кнопку 'Отменить'.""" + + self.page.mouse.click(10, 10) + self.wait_for_timeout(300) + + cancel_button = self.get_button_by_name("cancel") + if cancel_button: + cancel_button.click() + logger.debug("Clicked close button") + + def click_remove_button(self) -> None: + """Удаляет стойку с подтверждением.""" + + delete_button = self.get_button_by_name("delete") + if not delete_button: + return + + delete_button.click() + logger.debug("Clicked remove button") + self.wait_for_timeout(1500) + + self._confirm_deletion() + + def _confirm_deletion(self) -> None: + """Подтверждает удаление стойки.""" + + expected_title = "Удаление" + try: + self.delete_confirm.check_title( + title=expected_title, + msg=f"Expected title: '{expected_title}'" + ) + except AssertionError as e: + logger.warning(f"Dialog title mismatch: {e}") + + expected_message = f"Удалить {self.window_title}?" + try: + self.delete_confirm.check_text( + text=expected_message, + msg=f"Expected message: '{expected_message}'" + ) + except AssertionError as e: + logger.warning(f"Message text mismatch: {e}") + + self.delete_confirm.should_be_cancel_button() + self.delete_confirm.should_be_allow_button() + self.delete_confirm.click_allow_button() + self.wait_for_timeout(2000) + logger.debug("Remove confirmation completed") + + def click_done_button(self) -> None: + """Сохраняет изменения стойки.""" + + save_button = self.get_button_by_name("save") + if save_button: + save_button.click() + logger.debug("Clicked done button") + + def fill_rack_data(self, rack_data: RackEditData) -> dict: + """Заполняет поля формы редактирования стойки. + + Args: + rack_data: Данные для заполнения. + + Returns: + Словарь с результатами заполнения. + """ + + if self.active_tab != self.TAB_GENERAL: + self.switch_to_tab(self.TAB_GENERAL) + + results = { + "text_fields_filled": 0, + "combobox_fields_filled": 0, + "checkboxes_set": 0 + } + + self._fill_text_fields(rack_data, results) + self._fill_combobox_fields(rack_data, results) + self._set_checkbox(rack_data, results) + + return results + + def _fill_text_fields(self, rack_data: RackEditData, results: dict) -> None: + """Заполняет текстовые поля. + + Args: + rack_data: Данные для заполнения. + results: Словарь с результатами для обновления. + """ + + for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items(): + value = getattr(rack_data, attr_name, "") + if not value or not str(value).strip(): + continue + + self._fill_single_text_field(field_label, field_name, value, results) + + def _fill_single_text_field( + self, + field_label: str, + field_name: str, + value: str, + results: dict + ) -> None: + """Заполняет одно текстовое поле. + + Args: + field_label: Метка поля. + field_name: Имя поля. + value: Значение для заполнения. + results: Словарь с результатами. + """ + + try: + input_field = self.get_content_item(field_name) + if input_field: + input_field.input_value(value) + results["text_fields_filled"] += 1 + logger.info(f"Field '{field_label}' filled: '{value}'") + except Exception as e: + logger.error(f"Error filling field '{field_label}': {e}") + + def _fill_combobox_fields(self, rack_data: RackEditData, results: dict) -> None: + """Заполняет combobox поля. + + Args: + rack_data: Данные для заполнения. + results: Словарь с результатами для обновления. + """ + + for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items(): + value = getattr(rack_data, attr_name, "") + if not value or not str(value).strip(): + continue + + self._fill_single_combobox_field( + field_label, input_name, list_name, value, results + ) + + def _fill_single_combobox_field( + self, + field_label: str, + input_name: str, + list_name: str, + value: str, + results: dict + ) -> None: + """Заполняет одно combobox поле. + + Args: + field_label: Метка поля. + input_name: Имя поля ввода. + list_name: Имя списка. + value: Значение для выбора. + results: Словарь с результатами. + """ + + try: + combobox_field = self.get_content_item(input_name) + if not combobox_field: + return + + combobox_field.click(force=True) + self.wait_for_timeout(500) + + dropdown_list = self.get_content_item(list_name) + if dropdown_list: + dropdown_list.click_item_with_text(value) + results["combobox_fields_filled"] += 1 + logger.info(f"Field '{field_label}' set: '{value}'") + except Exception as e: + logger.error(f"Error filling combobox '{field_label}': {e}") + + def _set_checkbox(self, rack_data: RackEditData, results: dict) -> None: + """Устанавливает чекбокс. + + Args: + rack_data: Данные для заполнения. + results: Словарь с результатами для обновления. + """ + + if rack_data.ventilation_panel is None: + return + + try: + checkbox = self.get_content_item("ventilation_checkbox") + if not checkbox: + return + + if rack_data.ventilation_panel: + checkbox.check(force=True) + else: + checkbox.uncheck(force=True) + + results["checkboxes_set"] += 1 + logger.info(f"Checkbox 'Ventilation panel' set to: {rack_data.ventilation_panel}") + except Exception as e: + logger.error(f"Error setting checkbox: {e}") + + # Проверки + def verify_all_filled_fields( + self, + rack_data: RackEditData, + skip_fields: Optional[List[str]] = None + ) -> dict: + """Проверяет, что все поля заполнены корректно. + + Args: + rack_data: Данные для проверки. + skip_fields: Список полей, которые нужно пропустить. + + Returns: + Словарь с результатами проверки. + """ + + if self.active_tab != self.TAB_GENERAL: + self.switch_to_tab(self.TAB_GENERAL) + + results = { + "total_expected_fields": 0, + "correctly_filled": 0, + "incorrectly_filled": 0, + "not_filled": 0, + "skipped_fields": 0, + "field_errors": [] + } + + if skip_fields is None: + skip_fields = [] + + self._verify_text_fields(rack_data, skip_fields, results) + self._verify_combobox_fields(rack_data, skip_fields, results) + self._verify_checkbox(rack_data, skip_fields, results) + + if results["total_expected_fields"] > 0: + success_rate = results["correctly_filled"] / results["total_expected_fields"] * 100 + logger.info( + f"Field check: {results['correctly_filled']}/" + f"{results['total_expected_fields']} ({success_rate:.1f}%)" + ) + + return results + + def _verify_text_fields( + self, + rack_data: RackEditData, + skip_fields: List[str], + results: dict + ) -> None: + """Проверяет текстовые поля. + + Args: + rack_data: Данные для проверки. + skip_fields: Список полей для пропуска. + results: Словарь с результатами для обновления. + """ + + for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items(): + expected_value = getattr(rack_data, attr_name, "") + if not expected_value or not str(expected_value).strip(): + continue + + results["total_expected_fields"] += 1 + + if field_label in skip_fields: + results["skipped_fields"] += 1 + continue + + self._verify_single_text_field(field_label, field_name, expected_value, results) + + def _verify_single_text_field( + self, + field_label: str, + field_name: str, + expected_value: str, + results: dict + ) -> None: + """Проверяет одно текстовое поле. + + Args: + field_label: Метка поля. + field_name: Имя поля. + expected_value: Ожидаемое значение. + results: Словарь с результатами. + """ + + try: + input_field = self.get_content_item(field_name) + if not input_field: + results["not_filled"] += 1 + results["field_errors"].append(f"Field '{field_label}' input not found") + return + + actual_value = input_field.get_input_value() + if actual_value == expected_value: + results["correctly_filled"] += 1 + else: + results["incorrectly_filled"] += 1 + results["field_errors"].append( + f"Field '{field_label}': expected '{expected_value}', got '{actual_value}'" + ) + except Exception as e: + results["not_filled"] += 1 + results["field_errors"].append(f"Error checking field '{field_label}': {str(e)}") + + def _verify_combobox_fields( + self, + rack_data: RackEditData, + skip_fields: List[str], + results: dict + ) -> None: + """Проверяет combobox поля. + + Args: + rack_data: Данные для проверки. + skip_fields: Список полей для пропуска. + results: Словарь с результатами для обновления. + """ + + for field_label, (attr_name, _, _) in self.COMBOBOX_FIELDS_MAPPING.items(): + expected_value = getattr(rack_data, attr_name, "") + if not expected_value or not str(expected_value).strip(): + continue + + results["total_expected_fields"] += 1 + + if field_label in skip_fields: + results["skipped_fields"] += 1 + continue + + self._verify_single_combobox_field(field_label, expected_value, results) + + def _verify_single_combobox_field( + self, + field_label: str, + expected_value: str, + results: dict + ) -> None: + """Проверяет одно combobox поле. + + Args: + field_label: Метка поля. + expected_value: Ожидаемое значение. + results: Словарь с результатами. + """ + + try: + actual_value = self._get_combobox_value(field_label) + actual_clean = actual_value.strip() if actual_value else "" + expected_clean = expected_value.strip() if expected_value else "" + + if actual_clean == expected_clean: + results["correctly_filled"] += 1 + else: + results["incorrectly_filled"] += 1 + results["field_errors"].append( + f"Combobox '{field_label}': expected '{expected_clean}', got '{actual_clean}'" + ) + except Exception as e: + results["not_filled"] += 1 + results["field_errors"].append(f"Error checking combobox '{field_label}': {e}") + + def _get_combobox_value(self, field_label: str) -> str: + """Получает значение из combobox поля. + + Args: + field_label: Название поля. + + Returns: + Значение поля или пустая строка. + """ + + actual_value = "" + locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label) + + if not locator: + return actual_value + + element = self.page.locator(locator).first + if element.count() == 0: + return actual_value + + selections_container = element.locator( + "xpath=ancestor::div[contains(@class, 'v-select__selections')]" + ).first + + if selections_container.count() > 0: + value_span = selections_container.locator("span").first + actual_value = value_span.text_content() or "" + + return actual_value + + def _verify_checkbox( + self, + rack_data: RackEditData, + skip_fields: List[str], + results: dict + ) -> None: + """Проверяет чекбокс. + + Args: + rack_data: Данные для проверки. + skip_fields: Список полей для пропуска. + results: Словарь с результатами для обновления. + """ + + if rack_data.ventilation_panel is None: + return + + results["total_expected_fields"] += 1 + + if "Вентиляционная панель" in skip_fields: + results["skipped_fields"] += 1 + return + + try: + checkbox = self.get_content_item("ventilation_checkbox") + if not checkbox: + results["not_filled"] += 1 + results["field_errors"].append("Checkbox 'Ventilation panel' not found") + return + + checkbox_state = checkbox.is_checked() + if checkbox_state == rack_data.ventilation_panel: + results["correctly_filled"] += 1 + else: + results["incorrectly_filled"] += 1 + expected_status = "enabled" if rack_data.ventilation_panel else "disabled" + actual_status = "enabled" if checkbox_state else "disabled" + results["field_errors"].append( + f"Checkbox 'Ventilation panel': expected '{expected_status}', " + f"got '{actual_status}'" + ) + except Exception as e: + results["not_filled"] += 1 + results["field_errors"].append(f"Error checking checkbox: {str(e)}") diff --git a/locators/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 0000000..1e21bdc Binary files /dev/null and b/tests/e2e/elements/test_image.jpg differ 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()