Compare commits

...

4 Commits

26 changed files with 3679 additions and 1839 deletions

View File

@ -51,14 +51,14 @@ class BaseComponent:
fields_locators = {} fields_locators = {}
layouts = container_locator.locator("div.layout") layouts = container_locator.locator("div.layout > div.flex").locator("..")
for i in range(layouts.count()): for i in range(layouts.count()):
layout = layouts.nth(i) layout = layouts.nth(i)
flex_containers = layout.locator("div.flex") 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) label_container = flex_containers.nth(j)
input_container = flex_containers.nth(j + 1) input_container = flex_containers.nth(j + 1)
@ -72,7 +72,9 @@ class BaseComponent:
"input, textarea, select" "input, textarea, select"
).count() > 0 ).count() > 0
if has_input: not_found = fields_locators.get(label_text) is None
if has_input and not_found:
fields_locators[label_text] = input_container fields_locators[label_text] = input_container
return fields_locators return fields_locators

View File

@ -17,31 +17,48 @@ logger = get_logger("CONFIRM_WINDOW")
class ConfirmComponent(BaseComponent): class ConfirmComponent(BaseComponent):
"""Компонент окна подтверждения действий.""" """Компонент окна подтверждения действий."""
def __init__(self, page: Page, cancel_button_text: str, allow_button_text: str): def __init__(self, page: Page, cancel_button_text: str = "", allow_button_text: str = "",
cancel_button_locator: str = None, allow_button_locator: str = None):
"""Инициализация компонента. """Инициализация компонента.
Args: Args:
page: Экземпляр страницы Playwright. page: Экземпляр страницы Playwright.
cancel_button_text: Текст кнопки отмены. cancel_button_text: Текст кнопки отмены (по умолчанию пустая строка).
allow_button_text: Текст кнопки подтверждения. allow_button_text: Текст кнопки подтверждения (по умолчанию пустая строка).
cancel_button_locator: Локатор кнопки отмены (опционально).
allow_button_locator: Локатор кнопки подтверждения (опционально).
""" """
super().__init__(page) super().__init__(page)
self.title = Text(page, ConfirmLocators.TITLE, "confirm title") self.title = Text(page, ConfirmLocators.TITLE, "confirm title")
self.text = Text(page, ConfirmLocators.TEXT, "confirm text") self.text = Text(page, ConfirmLocators.TEXT, "confirm text")
self.close_button = Button(page, ConfirmLocators.BUTTON_CLOSE, "confirm close button") self.close_button = Button(page, ConfirmLocators.BUTTON_CLOSE, "confirm close button")
self.cancel_button = Button(
page, # Инициализация кнопок с приоритетом локаторам
page.get_by_role("button", name=cancel_button_text).first, if cancel_button_locator:
"confirm cancel button" self.cancel_button = Button(page, cancel_button_locator, "confirm cancel button")
) elif cancel_button_text:
self.allow_button = Button( self.cancel_button = Button(
page, page,
page.get_by_role("button", name=allow_button_text).first, page.get_by_role("button", name=cancel_button_text).first,
"confirm allow button" "confirm cancel button"
) )
else:
self.cancel_button = None
logger.warning("Cancel button not initialized - neither text nor locator specified")
if allow_button_locator:
self.allow_button = Button(page, allow_button_locator, "confirm allow button")
elif allow_button_text:
self.allow_button = Button(
page,
page.get_by_role("button", name=allow_button_text).first,
"confirm allow button"
)
else:
self.allow_button = None
logger.warning("Allow button not initialized - neither text nor locator specified")
# Действия: # Действия:
def click_allow_button(self) -> None: def click_allow_button(self) -> None:

View File

@ -214,15 +214,30 @@ class DropdownList(BaseComponent):
Raises: Raises:
AssertionError: Если элемент отсутствует или недоступен. 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) element = self.page.get_by_role("listitem").filter(has_text=text)
if element.count() > 1: if element.count() > 1:
rtext = f"^{text}$" rtext = f"^{text}$"
element = self.page.get_by_role("listitem").filter( element = self.page.get_by_role("listitem").filter(
has_text=re.compile(rtext) has_text=re.compile(rtext)
) )
enabled = element.is_enabled() if not element.first.is_enabled():
if not enabled:
assert False, f"Dropdown list item '{text}' is missing or disabled" assert False, f"Dropdown list item '{text}' is missing or disabled"
def check_vertical_scrolling(self, locator: str | Locator) -> bool: def check_vertical_scrolling(self, locator: str | Locator) -> bool:

View File

@ -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
# ...

View File

@ -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

View File

@ -68,6 +68,11 @@ class ModalWindowComponent(BaseComponent):
self.toolbar.click_button("close") self.toolbar.click_button("close")
def clear_content_items(self) -> None:
"""Очищает все элементы содержимого окна."""
self.content_items = {}
def scroll_window_down(self) -> None: def scroll_window_down(self) -> None:
"""Прокручивает содержимое окна вниз.""" """Прокручивает содержимое окна вниз."""

View File

@ -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"

View File

@ -38,7 +38,7 @@ class CreateChildElementFrame(BaseComponent):
has_text="Создать дочерний элемент в" has_text="Создать дочерний элемент в"
).get_by_role("button").nth(0) ).get_by_role("button").nth(0)
# Кнопка "Отменить" - используем рабочий локатор из старой версии # Кнопка "Отменить" -
cancel_button_locator = self.page.get_by_role("navigation").filter( cancel_button_locator = self.page.get_by_role("navigation").filter(
has_text=re.compile('Создать дочерний элемент в') has_text=re.compile('Создать дочерний элемент в')
).get_by_role("button").nth(1) ).get_by_role("button").nth(1)

View File

@ -35,7 +35,6 @@ class AddUserModalWindow(ModalWindowComponent):
super().__init__(page) super().__init__(page)
# Локаторы элементов формы # Локаторы элементов формы
text_field_locator = f"//{ModalWindowLocators.TEXT_FIELD_INPUT_FORM_USER_DATA}"
input_form_locator = ModalWindowLocators.INPUT_FORM_USER_DATA input_form_locator = ModalWindowLocators.INPUT_FORM_USER_DATA
# Настройка заголовка и кнопки закрытия тулбара # Настройка заголовка и кнопки закрытия тулбара
@ -49,7 +48,7 @@ class AddUserModalWindow(ModalWindowComponent):
self.add_toolbar_title(self.window_title) self.add_toolbar_title(self.window_title)
self.add_toolbar_button(locator_button_toolbar_close, "close") self.add_toolbar_button(locator_button_toolbar_close, "close")
elements_locators = self._get_fields_locators(page) elements_locators = self.get_input_fields_locators(page.locator(input_form_locator))
# Поле Тип авторизации # Поле Тип авторизации
loc = elements_locators.get("Тип авторизации") loc = elements_locators.get("Тип авторизации")
@ -58,7 +57,7 @@ class AddUserModalWindow(ModalWindowComponent):
self.add_content_item("auth_type_selector", auth_type_selector) self.add_content_item("auth_type_selector", auth_type_selector)
# Поле Имя # Поле Имя
loc = elements_locators.get("Имя").locator(text_field_locator) loc = elements_locators.get("Имя").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_NAME)
name_input = TextInput(page, loc, "name_input") name_input = TextInput(page, loc, "name_input")
self.add_content_item("name_input", name_input) self.add_content_item("name_input", name_input)
@ -76,34 +75,34 @@ class AddUserModalWindow(ModalWindowComponent):
# Чекбокс "Блокировка" # Чекбокс "Блокировка"
checkbox_blocking = Checkbox( checkbox_blocking = Checkbox(
page, page,
label_blocking_locator.locator("../..").get_by_role("checkbox"), page.locator(input_form_locator).locator(ModalWindowLocators.INPUT_FORM_USER_DATA_CHECKBOX_BLOCKED),
"blocking" "blocking"
) )
self.add_content_item("blocking_checkbox", checkbox_blocking) self.add_content_item("blocking_checkbox", checkbox_blocking)
# Поле Роль # Поле Роль
role_loc = elements_locators.get("Роль").get_by_role("combobox") role_loc = elements_locators.get("Роль").get_by_role("combobox").first
role_input = TextInput(page, role_loc, "role_input") role_input = TextInput(page, role_loc, "role_input")
self.add_content_item("role_input", role_input) self.add_content_item("role_input", role_input)
self.add_content_item("roles_list", DropdownList(page)) self.add_content_item("roles_list", DropdownList(page))
# Поле Пароль # Поле Пароль
loc = elements_locators.get("Пароль").locator(text_field_locator) loc = elements_locators.get("Пароль").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_PASSWORD)
password_input = TextInput(page, loc, "password_input") password_input = TextInput(page, loc, "password_input")
self.add_content_item("password_input", password_input) self.add_content_item("password_input", password_input)
# Поле Комментарий # Поле Комментарий
loc = elements_locators.get("Комментарий").locator(text_field_locator) loc = elements_locators.get("Комментарий").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_COMMENT)
commentary_input = TextInput(page, loc, "commentary_input") commentary_input = TextInput(page, loc, "commentary_input")
self.add_content_item("commentary_input", commentary_input) self.add_content_item("commentary_input", commentary_input)
# Поле E-mail # Поле E-mail
loc = elements_locators.get("E-mail").locator(text_field_locator) loc = elements_locators.get("E-mail").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_EMAIL)
email_input = TextInput(page, loc, "email_input") email_input = TextInput(page, loc, "email_input")
self.add_content_item("email_input", email_input) self.add_content_item("email_input", email_input)
# Поле Номер для СМС # Поле Номер для СМС
loc = elements_locators.get("Номер для СМС").locator(text_field_locator) loc = elements_locators.get("Номер для СМС").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_SMS)
phone_input = TextInput(page, loc, "phone_input") phone_input = TextInput(page, loc, "phone_input")
self.add_content_item("phone_input", phone_input) self.add_content_item("phone_input", phone_input)
@ -120,7 +119,7 @@ class AddUserModalWindow(ModalWindowComponent):
# Чекбокс "Подписка на Push-уведомления" # Чекбокс "Подписка на Push-уведомления"
checkbox_push = Checkbox( checkbox_push = Checkbox(
page, page,
label_push_locator.locator("../..").get_by_role("checkbox"), page.locator(input_form_locator).locator(ModalWindowLocators.INPUT_FORM_USER_DATA_CHECKBOX_PUSH_ACTIVE),
"push_notification" "push_notification"
) )
self.add_content_item("push_notification_checkbox", checkbox_push) self.add_content_item("push_notification_checkbox", checkbox_push)
@ -192,7 +191,8 @@ class AddUserModalWindow(ModalWindowComponent):
if auth_type == "LDAP": if auth_type == "LDAP":
menu_locator = self.page.locator(ModalWindowLocators.MENU_ACTIVE_INPUT_FORM) menu_locator = self.page.locator(ModalWindowLocators.MENU_ACTIVE_INPUT_FORM)
elements_locators = self._get_fields_locators(self.page) elements_locators = self.get_input_fields_locators(
self.page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA))
# Добавилось поле Группа # Добавилось поле Группа
group_loc = elements_locators.get("Группа").get_by_role("combobox") group_loc = elements_locators.get("Группа").get_by_role("combobox")
@ -223,7 +223,8 @@ class AddUserModalWindow(ModalWindowComponent):
search_button.click() search_button.click()
# Если в группе есть пользователи, открывается новое поле, заново вычисляем локаторы # Если в группе есть пользователи, открывается новое поле, заново вычисляем локаторы
elements_locators = self._get_fields_locators(self.page) elements_locators = self.get_input_fields_locators(
self.page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA))
users_ad_loc = elements_locators.get("Пользователи AD") users_ad_loc = elements_locators.get("Пользователи AD")
# users_ad_loc = elements_locators.get("Пользователи LDAP") # users_ad_loc = elements_locators.get("Пользователи LDAP")
assert users_ad_loc, f"Selected group {group_name} is empty. Use another group." assert users_ad_loc, f"Selected group {group_name} is empty. Use another group."
@ -323,39 +324,28 @@ class AddUserModalWindow(ModalWindowComponent):
def locators_recalculation(self, is_active_directory=False) -> None: def locators_recalculation(self, is_active_directory=False) -> None:
"""Пересчет локаторов полей ввода""" """Пересчет локаторов полей ввода"""
text_field_locator = f"//{ModalWindowLocators.TEXT_FIELD_INPUT_FORM_USER_DATA}" elements_locators = self.get_input_fields_locators(
self.page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA))
elements_locators = self._get_fields_locators(self.page) new_loc = elements_locators.get("Имя").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_NAME)
new_loc = elements_locators.get("Имя").locator(text_field_locator)
self.get_content_item("name_input").update_locator(new_loc) self.get_content_item("name_input").update_locator(new_loc)
if not is_active_directory: if not is_active_directory:
new_loc = elements_locators.get("Пароль").locator(text_field_locator) new_loc = elements_locators.get("Пароль").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_PASSWORD)
self.get_content_item("password_input").update_locator(new_loc) self.get_content_item("password_input").update_locator(new_loc)
new_loc = elements_locators.get("Роль").get_by_role("combobox") new_loc = elements_locators.get("Роль").get_by_role("combobox").first
self.get_content_item("role_input").update_locator(new_loc) self.get_content_item("role_input").update_locator(new_loc)
new_loc = elements_locators.get("Комментарий").locator(text_field_locator) new_loc = elements_locators.get("Комментарий").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_COMMENT)
self.get_content_item("commentary_input").update_locator(new_loc) self.get_content_item("commentary_input").update_locator(new_loc)
new_loc = elements_locators.get("E-mail").locator(text_field_locator) new_loc = elements_locators.get("E-mail").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_EMAIL)
self.get_content_item("email_input").update_locator(new_loc) self.get_content_item("email_input").update_locator(new_loc)
new_loc = elements_locators.get("Номер для СМС").locator(text_field_locator) new_loc = elements_locators.get("Номер для СМС").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_SMS)
self.get_content_item("phone_input").update_locator(new_loc) self.get_content_item("phone_input").update_locator(new_loc)
def _get_fields_locators(self, page) -> dict:
fields_locators = {}
elements = page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA). \
locator("div.v-text-field__slot > input").all()
for el in elements:
val = el.input_value().strip()
if val:
fields_locators[val] = el.locator("../ancestor::div[5]")
return fields_locators
# Проверки: # Проверки:
def check_content(self): def check_content(self):
"""Проверяет наличие и корректность всех элементов формы создания локального пользователя. """Проверяет наличие и корректность всех элементов формы создания локального пользователя.
@ -419,8 +409,8 @@ class AddUserModalWindow(ModalWindowComponent):
elif name == "roles_list": elif name == "roles_list":
continue continue
else: else:
print(f"check item: {name}") # print(f"check item: {name}")
print(item) # print(item)
item.check_visibility( item.check_visibility(
f"Modal window content item with name '{name}' is missing" f"Modal window content item with name '{name}' is missing"
) )
@ -443,7 +433,8 @@ class AddUserModalWindow(ModalWindowComponent):
if auth_type_selector: if auth_type_selector:
self.select_auth_type("LDAP") self.select_auth_type("LDAP")
elements_locators = self._get_fields_locators(self.page) elements_locators = self.get_input_fields_locators(
self.page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA))
# Добавилось поле Группа # Добавилось поле Группа
group_loc = elements_locators.get("Группа").get_by_role("combobox") group_loc = elements_locators.get("Группа").get_by_role("combobox")

File diff suppressed because it is too large Load Diff

View File

@ -34,8 +34,6 @@ class EditUserModalWindow(ModalWindowComponent):
super().__init__(page) super().__init__(page)
# Локаторы элементов формы # Локаторы элементов формы
# text_field_locator = ModalWindowLocators.TEXT_FIELD_INPUT_FORM_USER_DATA
text_field_locator = f"xpath={ModalWindowLocators.TEXT_FIELD_INPUT_FORM_USER_DATA}"
input_form_locator = ModalWindowLocators.INPUT_FORM_USER_DATA input_form_locator = ModalWindowLocators.INPUT_FORM_USER_DATA
# Настройка заголовка и кнопки закрытия # Настройка заголовка и кнопки закрытия
@ -50,31 +48,32 @@ class EditUserModalWindow(ModalWindowComponent):
self.add_toolbar_button(locator_button_toolbar_close, "close") self.add_toolbar_button(locator_button_toolbar_close, "close")
# Добавление полей формы # Добавление полей формы
elements_locators = self._get_fields_locators(page) elements_locators = self.get_input_fields_locators(
self.page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA))
# Поле Имя # Поле Имя
loc = elements_locators.get("Имя").locator(text_field_locator) loc = elements_locators.get("Имя").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_NAME)
name_input = TextInput(page, loc, "name_input") name_input = TextInput(page, loc, "name_input")
self.add_content_item("name_input", name_input) self.add_content_item("name_input", name_input)
# Поле Роль # Поле Роль
role_loc = self.page.locator(input_form_locator).get_by_role("combobox") role_loc = self.page.locator(input_form_locator).get_by_role("combobox").first
role_input = TextInput(page, role_loc, "role_input") role_input = TextInput(page, role_loc, "role_input")
self.add_content_item("role_input", role_input) self.add_content_item("role_input", role_input)
self.add_content_item("roles_list", DropdownList(page)) self.add_content_item("roles_list", DropdownList(page))
# Поле Комментарий # Поле Комментарий
loc = elements_locators.get("Комментарий").locator(text_field_locator) loc = elements_locators.get("Комментарий").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_COMMENT)
commentary_input = TextInput(page, loc, "commentary_input") commentary_input = TextInput(page, loc, "commentary_input")
self.add_content_item("commentary_input", commentary_input) self.add_content_item("commentary_input", commentary_input)
# Поле E-mail # Поле E-mail
loc = elements_locators.get("E-mail").locator(text_field_locator) loc = elements_locators.get("E-mail").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_EMAIL)
email_input = TextInput(page, loc, "email_input") email_input = TextInput(page, loc, "email_input")
self.add_content_item("email_input", email_input) self.add_content_item("email_input", email_input)
# Поле Номер для СМС # Поле Номер для СМС
loc = elements_locators.get("Номер для СМС").locator(text_field_locator) loc = elements_locators.get("Номер для СМС").locator(ModalWindowLocators.INPUT_FORM_USER_DATA_FIELD_SMS)
phone_input = TextInput(page, loc, "phone_input") phone_input = TextInput(page, loc, "phone_input")
self.add_content_item("phone_input", phone_input) self.add_content_item("phone_input", phone_input)
@ -93,7 +92,7 @@ class EditUserModalWindow(ModalWindowComponent):
# Чекбокс "Блокировка" # Чекбокс "Блокировка"
checkbox_blocking = Checkbox( checkbox_blocking = Checkbox(
page, page,
label_blocking_locator.locator("../..").get_by_role("checkbox"), page.locator(input_form_locator).locator(ModalWindowLocators.INPUT_FORM_USER_DATA_CHECKBOX_BLOCKED),
"blocking" "blocking"
) )
self.add_content_item("blocking_checkbox", checkbox_blocking) self.add_content_item("blocking_checkbox", checkbox_blocking)
@ -111,7 +110,7 @@ class EditUserModalWindow(ModalWindowComponent):
# Чекбокс "Подписка на Push-уведомления" # Чекбокс "Подписка на Push-уведомления"
checkbox_push = Checkbox( checkbox_push = Checkbox(
page, page,
label_push_locator.locator("../..").get_by_role("checkbox"), page.locator(input_form_locator).locator(ModalWindowLocators.INPUT_FORM_USER_DATA_CHECKBOX_PUSH_ACTIVE),
"push_notification" "push_notification"
) )
self.add_content_item("push_notification_checkbox", checkbox_push) self.add_content_item("push_notification_checkbox", checkbox_push)
@ -241,15 +240,15 @@ class EditUserModalWindow(ModalWindowComponent):
reset_password_button = self.get_button_by_name("reset_password") reset_password_button = self.get_button_by_name("reset_password")
reset_password_button.click() reset_password_button.click()
def _get_fields_locators(self, page) -> dict: # def _get_fields_locators(self, page) -> dict:
fields_locators = {} # fields_locators = {}
elements = page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA). \ # elements = page.locator(ModalWindowLocators.INPUT_FORM_USER_DATA). \
locator("div.v-text-field__slot > input").all() # locator("div.v-text-field__slot > input").all()
for el in elements: # for el in elements:
val = el.input_value().strip() # val = el.input_value().strip()
if val: # if val:
fields_locators[val] = el.locator("../ancestor::div[5]") # fields_locators[val] = el.locator("../ancestor::div[5]")
return fields_locators # return fields_locators
# Проверки: # Проверки:
def check_content(self, user_name, role): def check_content(self, user_name, role):

View File

@ -15,6 +15,8 @@ class ConfirmLocators:
""" """
CONFIRM = "//div[contains(@class, 'v-dialog--active')]" CONFIRM = "//div[contains(@class, 'v-dialog--active')]"
TITLE = "//div[@class='v-card__title']/h3" TITLE = f"{CONFIRM}//div[contains(@class, 'v-card__title')]"
#TITLE = "//div[@class='v-card__title']/h3"
BUTTON_CLOSE = "//div[@class='vuedl-layout__closeBtn']" BUTTON_CLOSE = "//div[@class='vuedl-layout__closeBtn']"
TEXT = f"{CONFIRM}/div[2]/div[@class='v-card__text']" #TEXT = f"{CONFIRM}/div[2]/div[@class='v-card__text']"
TEXT = f"{CONFIRM}//div[contains(@class, 'v-card__text')]"

View File

@ -23,15 +23,8 @@ class ModalWindowLocators:
MODAL_WINDOW_TITLE = f"{MODAL_WINDOW}//div[contains(@class, 'v-toolbar__title')]" MODAL_WINDOW_TITLE = f"{MODAL_WINDOW}//div[contains(@class, 'v-toolbar__title')]"
MODAL_WINDOW_TEXT_FIELD_INPUT = f"{MODAL_WINDOW}//input" MODAL_WINDOW_TEXT_FIELD_INPUT = f"{MODAL_WINDOW}//input"
INPUT_FORM_USER_DATA = f"{MODAL_WINDOW}//form[@class='v-form']" INPUT_FORM_USER_DATA = "//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"
# 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')]"
MENU_ACTIVE_INPUT_FORM = "//div[contains(@class, 'menuable__content__active')]"
MENU_ACTIVE_ITEMS = "//div[@role='list']//div[@role='listitem']"
LABEL_INPUT_FORM_USER_DATA = "//label[contains(@class,'v-label')]/span" 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']"

View File

@ -31,38 +31,36 @@ class RackLocators:
# Контейнер формы создания/редактирования стойки # Контейнер формы создания/редактирования стойки
FORM_INPUT_CONTAINER = "//div[contains(@class, 'flex xs6 pa-0')]" FORM_INPUT_CONTAINER = "//div[contains(@class, 'flex xs6 pa-0')]"
# Локаторы полей формы создания стойки # Форма редактирования стойки в модальном окне
RACK_NAME_FIELD = ("//div[contains(@class, 'container')]" RACK_EDIT_FORM = "[data-testid='cabinet-bar__cabinet-form']"
"//label[text()='Имя']/following-sibling::input")
RACK_HEIGHT_FIELD = ("//div[contains(@class, 'container')]"
"//div[contains(@class, 'v-input__slot') and "
".//label[text()='Высота в юнитах']]")
RACK_DEPTH_FIELD = ("//div[contains(@class, 'container')]"
"//div[contains(@class, 'v-input__slot') and "
".//label[text()='Глубина (мм)']]")
RACK_SERIAL_FIELD = ("//div[contains(@class, 'container')]"
"//label[text()='Серийный номер']/following-sibling::input")
RACK_INVENTORY_FIELD = ("//div[contains(@class, 'container')]"
"//label[text()='Инвентарный номер']/following-sibling::input")
RACK_COMMENT_FIELD = ("//div[contains(@class, 'container')]"
"//label[text()='Комментарий']/following-sibling::input")
RACK_CABLE_ENTRY_FIELD = ("//div[contains(@class, 'container')]"
"//div[contains(@class, 'v-input__slot') and "
".//label[text()='Ввод кабеля']]")
RACK_STATE_FIELD = ("//div[contains(@class, 'container')]"
"//div[contains(@class, 'v-input__slot') and "
".//label[text()='Состояние']]")
RACK_OWNER_FIELD = ("//div[contains(@class, 'container')]"
"//div[contains(@class, 'v-input__slot') and "
".//label[text()='Владелец']]")
RACK_SERVICE_ORG_FIELD = ("//div[contains(@class, 'container')]"
"//div[contains(@class, 'v-input__slot') and "
".//label[text()='Обслуживающая организация']]")
RACK_PROJECT_FIELD = ("//div[contains(@class, 'container')]"
"//div[contains(@class, 'v-input__slot') and "
".//label[text()='Проект/Титул']]")
# Локаторы для выпадающего меню # Локаторы полей формы
INPUT_FORM_RACK_DATA = f"{RACK_EDIT_FORM}"
INPUT_FORM_RACK_DATA_FIELD_NAME = "[data-testid='cabinet-bar__main__text-field__name']"
INPUT_FORM_RACK_DATA_FIELD_COMMENT = "[data-testid='cabinet-bar__main__text-field__comment']"
INPUT_FORM_RACK_DATA_FIELD_SERIAL = "[data-testid='cabinet-bar__main__text-field__serial_number']"
INPUT_FORM_RACK_DATA_FIELD_INVENTORY = "[data-testid='cabinet-bar__main__text-field__inventory_number']"
INPUT_FORM_RACK_DATA_FIELD_POWER = "[data-testid='cabinet-bar__main__text-field__allocated_power']"
# Локаторы для combobox полей
INPUT_FORM_RACK_DATA_FIELD_CABLE_ENTRY = "[data-testid='cabinet-bar__select_enum__select-field__cable_input']"
INPUT_FORM_RACK_DATA_FIELD_CONDITION_TYPE = "[data-testid='cabinet-bar__select_enum__select-field__condition_type']"
INPUT_FORM_RACK_DATA_FIELD_DEPTH = "[data-testid='cabinet-bar__select_enum__select-field__depth']"
INPUT_FORM_RACK_DATA_FIELD_USIZE = "[data-testid='cabinet-bar__select_enum__select-field__usize']"
INPUT_FORM_RACK_DATA_FIELD_OWNER = "[data-testid='cabinet-bar__select__select-field__owner']"
INPUT_FORM_RACK_DATA_FIELD_SERVICE_PROVIDER = "[data-testid='cabinet-bar__select__select-field__service_provider']"
INPUT_FORM_RACK_DATA_FIELD_PROJECT = "[data-testid='cabinet-bar__select__select-field__project']"
# Чекбоксы
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel'] input[type='checkbox']"
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_LABEL = "label:has-text('Вентиляционная панель')"
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_CONTAINER = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel']"
# Локаторы для меню combobox
MENU_ACTIVE_RACK_FORM = "//div[contains(@class, 'menuable__content__active')]"
MENU_ACTIVE_ITEMS = "//div[@role='list']//div[@role='listitem']"
# Локаторы для выпадающего меню (которые использовались в старом коде)
DROPDOWN_LIST = 'div.menuable__content__active div[role="list"]' DROPDOWN_LIST = 'div.menuable__content__active div[role="list"]'
DROPDOWN_ITEM_BY_TEXT = ('div.menuable__content__active ' DROPDOWN_ITEM_BY_TEXT = ('div.menuable__content__active '
'div[role="listitem"]:has(span:has-text("{}"))') 'div[role="listitem"]:has(span:has-text("{}"))')
@ -126,3 +124,34 @@ class RackLocators:
# Кнопки подтверждения удаления # Кнопки подтверждения удаления
CONFIRM_REMOVE_YES_BUTTON = "[data-testid='cabinet-bar__card_confirmation__btn__yes']" CONFIRM_REMOVE_YES_BUTTON = "[data-testid='cabinet-bar__card_confirmation__btn__yes']"
CONFIRM_REMOVE_NO_BUTTON = "[data-testid='cabinet-bar__card_confirmation__btn__no']" 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']"

View File

@ -1,355 +0,0 @@
"""Модуль страницы создания дочернего элемента.
Содержит класс для работы с формой создания дочернего элемента.
"""
from playwright.sync_api import Page, expect
from elements.tooltip_button_element import TooltipButton
from components.toolbar_component import ToolbarComponent
from components.dropdown_list_component import DropdownList
from pages.base_page import BasePage
from tools.logger import get_logger
logger = get_logger("CREATE_CHILD_ELEMENT")
# =============== Локаторы ================================================
PANEL_HEADER = "//span[text()='Объекты']/following-sibling::i"
TOOLBAR_CONTENT = "//div[@class='v-toolbar__content']"
CREATE_BUTTON_ANCESTOR_DIV3 = "xpath=/ancestor::div[3]//button"
PANEL_HEADER_ANCESTOR_DIV2 = "xpath=/ancestor::div[2]"
CREATE_CHILD_TITLE = "//div[contains(@class, 'v-toolbar__title') and contains(., 'Создать дочерний элемент в')]"
OBJECT_CLASS_COMBOBOX = "//div[@role='combobox' and .//label[text()='Класс объекта учета']]"
CANCEL_BUTTON = "//div[contains(@class, 'v-toolbar__title') and contains(., 'Создать дочерний элемент в')]/..//button[contains(@class, 'v-btn--icon')]"
# Локаторы для работы с combobox
COMBOBOX_LABEL = "label"
COMBOBOX_INPUT = "input[name='entity']"
COMBOBOX_ICON = ".v-input__icon--append"
COMBOBOX_ICON_ARROW = ".v-input__icon--append .mdi-menu-down"
# Локаторы для выпадающего списка combobox - уточненные
LISTBOX_SELECTOR = "//div[contains(@class, 'v-menu__content')]//div[@role='list']"
OPTIONS_SELECTOR = "//div[contains(@class, 'v-menu__content')]//div[@role='listitem']//span"
# Локаторы для получения выбранного значения
SELECTED_VALUE_SPAN = "span"
#========================================================================================================
class CreateChildElementTab(BasePage):
"""Класс для работы с формой создания дочернего элемента."""
def __init__(self, page: Page) -> None:
"""
Инициализирует объект формы создания дочернего элемента.
Args:
page: Экземпляр страницы Playwright
"""
super().__init__(page)
# Локаторы для кнопок
panel_header_locator = self.page.locator(PANEL_HEADER)
# Кнопка "Создать" - первая кнопка в тулбаре
create_button_locator = panel_header_locator.locator(CREATE_BUTTON_ANCESTOR_DIV3).nth(0)
# Кнопка "Отменить" - ищем глобально на странице
cancel_button_locator = self.page.locator(CANCEL_BUTTON)
# Инициализация кнопок
self.create_button = TooltipButton(page, create_button_locator, "add")
self.cancel_button = TooltipButton(page, cancel_button_locator, "cancel")
# Инициализация тулбара с обеими кнопками
self.toolbar = ToolbarComponent(page, "")
self.toolbar.add_tooltip_button(create_button_locator, "add")
self.toolbar.add_tooltip_button(cancel_button_locator, "cancel")
# Инициализация компонента выпадающего списка
self.dropdown = DropdownList(page)
def get_toolbar_title(self) -> list[str]:
"""
Получает заголовок панели инструментов.
Returns:
list[str]: Список элементов заголовка панели инструментов
"""
toolbar_title_locator = self.page.locator(PANEL_HEADER).\
locator(PANEL_HEADER_ANCESTOR_DIV2).get_by_role("navigation").\
locator(TOOLBAR_CONTENT)
return self.toolbar.get_toolbar_composite_title_text(toolbar_title_locator)
def should_be_toolbar_buttons(self) -> None:
"""
Проверяет наличие и функциональность кнопок тулбара.
Raises:
AssertionError: Если кнопки недоступны или подсказки неверны.
"""
self.wait_for_timeout(2000)
self.toolbar.check_button_visibility("cancel")
self.toolbar.check_button_tooltip("cancel", "Отменить")
self.toolbar.get_button_by_name("cancel").click()
self.wait_for_timeout(2000)
def click_create_button(self) -> None:
"""
Кликает на кнопку 'Создать'.
"""
logger.info("Клик на кнопку 'Создать'...")
self.toolbar.get_button_by_name("add").click()
def click_cancel_button(self) -> None:
"""
Кликает на кнопку 'Отменить'.
"""
logger.info("Клик на кнопку 'Отменить'...")
self.toolbar.get_button_by_name("cancel").click()
def check_toolbar_title(self, expected_title: str) -> None:
"""
Проверяет заголовок тулбара.
Args:
expected_title: Ожидаемый заголовок тулбара
Raises:
AssertionError: Если заголовок не соответствует ожидаемому
"""
# Используем метод тулбара с нашим специфичным локатором
self.toolbar.check_toolbar_presence_by_locator(CREATE_CHILD_TITLE,
f"Заголовок тулбара '{expected_title}' не найден")
# Получаем текст и проверяем его
actual_text = self.toolbar.get_toolbar_title_text(CREATE_CHILD_TITLE)
assert expected_title in actual_text, f"Заголовок не совпадает. Ожидалось: '{expected_title}', Получено: '{actual_text}'"
logger.info(f"Заголовок тулбара корректен: '{actual_text}'")
def check_object_class_combobox_presence(self) -> None:
"""
Проверяет наличие combobox 'Класс объекта учета'.
Raises:
AssertionError: Если combobox не найден
"""
logger.info("Проверка наличия combobox 'Класс объекта учета'...")
combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX)
expect(combobox_locator).to_be_visible()
logger.info("Combobox 'Класс объекта учета' найден")
def check_object_class_combobox_content(self) -> None:
"""
Проверяет содержимое combobox 'Класс объекта учета'.
Raises:
AssertionError: Если содержимое не соответствует ожидаемому
"""
logger.info("Проверка содержимого combobox 'Класс объекта учета'...")
combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX)
# Проверяем что combobox видим
expect(combobox_locator).to_be_visible()
# Проверяем наличие label
label_locator = combobox_locator.locator(COMBOBOX_LABEL)
expect(label_locator).to_have_text("Класс объекта учета")
# Проверяем наличие input поля
input_locator = combobox_locator.locator(COMBOBOX_INPUT)
expect(input_locator).to_be_visible()
# Для combobox нормально иметь readonly атрибут - это стандартное поведение
# Проверяем что поле доступно для выбора (не disabled)
expect(input_locator).not_to_have_attribute("disabled", "disabled")
# Проверяем наличие иконки стрелки
icon_locator = combobox_locator.locator(COMBOBOX_ICON_ARROW)
expect(icon_locator).to_be_visible()
logger.info("Содержимое combobox 'Класс объекта учета' корректно")
def open_object_class_combobox(self) -> None:
"""
Открывает выпадающий список combobox 'Класс объекта учета'.
"""
logger.info("Открытие combobox 'Класс объекта учета'...")
combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX)
listbox_locator = self.page.locator(LISTBOX_SELECTOR)
icon_locator = combobox_locator.locator(COMBOBOX_ICON)
# Проверяем, не открыт ли уже список
listbox_already_open = False
listbox_count = listbox_locator.count()
if listbox_count > 0:
listbox_already_open = listbox_locator.first.is_visible()
if not listbox_already_open:
# Только если список не открыт, кликаем на иконку
icon_locator.click(timeout=10000)
logger.info("Клик на иконку combobox выполнен")
self.wait_for_timeout(1000)
# Проверяем что список открылся
listbox_count_after = listbox_locator.count()
listbox_visible = False
if listbox_count_after > 0:
listbox_visible = listbox_locator.first.is_visible()
if listbox_visible:
logger.info("Выпадающий список найден и открыт")
else:
logger.warning("Не удалось открыть выпадающий список")
def get_object_class_options(self) -> list[str]:
"""
Получает список доступных опций из combobox.
Returns:
list[str]: Список доступных классов объектов
"""
logger.info("Получение списка опций combobox 'Класс объекта учета'...")
# Открываем combobox (если еще не открыт)
self.open_object_class_combobox()
# Используем метод get_item_names из DropdownList
options_list = self.dropdown.get_item_names(LISTBOX_SELECTOR)
# Закрываем combobox (кликаем вне его)
self.page.mouse.click(10, 10)
self.wait_for_timeout(500)
logger.info(f"Найдено опций: {len(options_list)} - {options_list}")
return options_list
def select_object_class(self, class_name: str) -> None:
"""
Выбирает класс объекта из выпадающего списка.
Args:
class_name: Название класса объекта для выбора
Raises:
AssertionError: Если класс не найден в списке
"""
logger.info(f"Выбор класса объекта: '{class_name}'...")
# Открываем combobox
self.open_object_class_combobox()
self.dropdown.click_item_with_text(class_name)
# Проверяем что выбор произошел
self.wait_for_timeout(1000)
selected_value = self.get_selected_object_class()
if class_name.lower() not in selected_value.lower() and selected_value.lower() not in class_name.lower():
# Если выбор не произошел, получаем доступные опции для отладки
available_options = self.get_object_class_options()
logger.warning(f"Класс '{class_name}' не выбран. Текущее значение: '{selected_value}'. Доступные опции: {available_options}")
raise AssertionError(f"Не удалось выбрать класс объекта '{class_name}'")
logger.info(f"Класс объекта '{class_name}' успешно выбран")
def get_selected_object_class(self) -> str:
"""
Получает выбранный класс объекта учета.
Returns:
str: Выбранный класс объекта или пустая строка если ничего не выбрано
"""
combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX)
selected_value = ""
# Ищем в span элементах
span_locator = combobox_locator.locator(SELECTED_VALUE_SPAN)
if span_locator.count() > 0:
for i in range(span_locator.count()):
span_text = span_locator.nth(i).text_content().strip()
if span_text and span_text not in ["Класс объекта учета"]:
selected_value = span_text
break
logger.info(f"Выбранный класс объекта: '{selected_value}'")
return selected_value
def check_object_class_selected(self, expected_class: str) -> None:
"""
Проверяет что выбран указанный класс объекта.
Args:
expected_class: Ожидаемый выбранный класс объекта
Raises:
AssertionError: Если выбранный класс не соответствует ожидаемому
"""
logger.info(f"Проверка выбранного класса объекта: '{expected_class}'...")
# Даем время на обновление значения
self.wait_for_timeout(1000)
actual_class = self.get_selected_object_class()
# Проверка - допускаем частичное совпадение
if expected_class.lower() in actual_class.lower() or actual_class.lower() in expected_class.lower():
logger.info(f"Класс объекта '{expected_class}' успешно выбран (фактически: '{actual_class}')")
else:
raise AssertionError(f"Выбранный класс не соответствует ожидаемому. Ожидалось: '{expected_class}', Получено: '{actual_class}'")
def check_object_class_options_content(self, expected_options: list = None) -> None:
"""
Проверяет содержимое списка опций combobox.
Args:
expected_options: Ожидаемый список опций. Если None, проверяет только что список не пустой.
Raises:
AssertionError: Если список опций не соответствует ожидаемому
"""
logger.info("Проверка содержимого списка опций combobox...")
# Получаем доступные опции
available_options = self.get_object_class_options()
if expected_options is not None:
# Проверяем соответствие ожидаемому списку
assert set(available_options) == set(expected_options), (
f"Список опций не соответствует ожидаемому. "
f"Ожидалось: {expected_options}, Получено: {available_options}"
)
else:
# Проверяем что список не пустой
assert len(available_options) > 0, "Список опций combobox пустой"
logger.info(f"Содержимое списка опций корректно: {available_options}")
def check_dropdown_item_presence(self, item_text: str) -> None:
"""
Проверяет наличие элемента в выпадающем списке.
Args:
item_text: Текст элемента для проверки
"""
logger.info(f"Проверка наличия элемента '{item_text}' в выпадающем списке...")
# Получаем все опции и проверяем наличие
available_options = self.get_object_class_options()
if item_text not in available_options:
raise AssertionError(f"Элемент '{item_text}' не найден в списке опций. Доступные опции: {available_options}")
logger.info(f"Элемент '{item_text}' присутствует в списке")

View File

@ -1,678 +0,0 @@
"""Модуль страницы создания дочернего элемента.
Содержит класс для работы с формой создания дочернего элемента.
"""
import re
from playwright.sync_api import Page, expect
from elements.tooltip_button_element import TooltipButton
from components.toolbar_component import ToolbarComponent
from components_derived.selection_bar_component import SelectionBarComponent
from pages.main_page import MainPage
from pages.base_page import BasePage
from components.base_component import BaseComponent
from components.alert_component import AlertComponent
from components.navbar_component import NavigationPanelComponent
from locators.navigation_panel_locators import NavigationPanelLocators
from locators.combobox_locators import ComboboxLocators
from locators.rack_locators import RackLocators
from locators.alert_locators import AlertLocators
from tools.logger import get_logger
logger = get_logger("CREATE_RACK_ELEMENT")
# Словарь для сопоставления названий полей с локаторами
COMBOBOX_FIELDS_MAP = {
# Обязательные поля
"Имя": RackLocators.RACK_NAME_FIELD,
"Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD,
"Глубина (мм)": RackLocators.RACK_DEPTH_FIELD,
# Дополнительные текстовые поля
"Серийный номер": RackLocators.RACK_SERIAL_FIELD,
"Инвентарный номер": RackLocators.RACK_INVENTORY_FIELD,
"Комментарий": RackLocators.RACK_COMMENT_FIELD,
# Combobox поля
"Ввод кабеля": RackLocators.RACK_CABLE_ENTRY_FIELD,
"Состояние": RackLocators.RACK_STATE_FIELD,
"Владелец": RackLocators.RACK_OWNER_FIELD,
"Обслуживающая организация": RackLocators.RACK_SERVICE_ORG_FIELD,
"Проект/Титул": RackLocators.RACK_PROJECT_FIELD
}
class CreateRackElementTab(BasePage):
"""Класс для работы с формой создания дочернего элемента."""
def __init__(self, page: Page) -> None:
"""
Инициализирует объект формы создания дочернего элемента.
Args:
page: Экземпляр страницы Playwright
"""
super().__init__(page)
# Инициализация BaseComponent
self.base_component = BaseComponent(page)
# Инициализация AlertComponent
self.alert = AlertComponent(page)
# Инициализация MainPage для работы с навигацией
self.main_page = MainPage(page)
# Инициализация NavigationPanelComponent
self.navigation_panel = NavigationPanelComponent(page)
# Кнопка "Добавить" - первая кнопка в тулбаре
create_button_locator = self.page.get_by_role("navigation").filter(has_text=re.compile('Создать дочерний элемент в')).get_by_role("button").nth(0)
# Кнопка "Отменить" - вторая кнопка в тулбаре
cancel_button_locator = self.page.get_by_role("navigation").filter(has_text=re.compile('Создать дочерний элемент в')).get_by_role("button").nth(1)
# Инициализация кнопок
self.create_button = TooltipButton(page, create_button_locator, "add")
self.cancel_button = TooltipButton(page, cancel_button_locator, "cancel")
# Инициализация тулбара с обеими кнопками
self.toolbar = ToolbarComponent(page, "Создать дочерний элемент в")
self.toolbar.add_tooltip_button(create_button_locator, "add")
self.toolbar.add_tooltip_button(cancel_button_locator, "cancel")
# Инициализация компонента панели выбора значения для работы с combobox
self.selection_bar = SelectionBarComponent(page, ComboboxLocators.OBJECT_CLASS_COMBOBOX)
# =============== МЕТОДЫ ДЕЙСТВИЙ ========================
def click_add_button(self) -> None:
"""
Кликает на кнопку 'Добавить'.
"""
self.toolbar.click_button("add")
def click_cancel_button(self) -> None:
"""
Кликает на кнопку 'Отменить'.
"""
self.toolbar.click_button("cancel")
def open_object_class_combobox(self) -> None:
"""
Открывает выпадающий список combobox 'Класс объекта учета'.
"""
logger.info("Открытие combobox 'Класс объекта учета'...")
self.selection_bar.open_values_list()
def select_object_class(self, class_name: str) -> None:
"""
Выбирает класс объекта из выпадающего списка.
Args:
class_name: Название класса объекта для выбора
Raises:
AssertionError: Если класс не найден в списке
"""
logger.info(f"Выбор класса объекта: '{class_name}'...")
# Открываем combobox
self.open_object_class_combobox()
# Выбираем значение из списка
self.selection_bar.select_value(class_name)
# Проверяем что выбор произошел
self.wait_for_timeout(1000)
selected_value = self.get_selected_object_class()
if class_name.lower() not in selected_value.lower() and selected_value.lower() not in class_name.lower():
# Если выбор не произошел, получаем доступные опции для отладки
available_options = self.get_object_class_options()
logger.warning(f"Класс '{class_name}' не выбран. Текущее значение: '{selected_value}'. Доступные опции: {available_options}")
raise AssertionError(f"Не удалось выбрать класс объекта '{class_name}'")
logger.info(f"Класс объекта '{class_name}' успешно выбран")
def get_object_class_options(self) -> list[str]:
"""
Получает список доступных опций из combobox.
Returns:
list[str]: Список доступных классов объектов
"""
logger.info("Получение списка опций combobox 'Класс объекта учета'...")
available_options = self.selection_bar.get_available_options()
logger.info(f"Доступные опции класса объекта: {available_options}")
return available_options
def get_selected_object_class(self) -> str:
"""
Получает выбранный класс объекта учета.
Returns:
str: Выбранный класс объекта или пустая строка если ничего не выбрано
"""
# Получаем заголовок панели выбора
return self.selection_bar.get_selection_bar_title()
def fill_rack_data(self, name: str, height: str = "42", depth: str = "1000",
serial: str = "", inventory: str = "", comment: str = "",
cable_entry: str = "", state: str = "", owner: str = "",
service_org: str = "", project: str = "") -> None:
"""
Заполняет данные для создания стойки.
Args:
name: Наименование стойки
height: Высота в юнитах (по умолчанию 42)
depth: Глубина в мм (по умолчанию 1000)
serial: Серийный номер
inventory: Инвентарный номер
comment: Комментарий
cable_entry: Ввод кабеля
state: Состояние
owner: Владелец
service_org: Обслуживающая организация
project: Проект/Титул
"""
logger.info(f"Заполнение данных стойки: {name}")
# Заполняем обязательные поля
name_field = self.page.locator(RackLocators.RACK_NAME_FIELD)
name_field.fill(name)
logger.info(f"Заполнено поле 'Имя': {name}")
self._select_combobox("Высота в юнитах", height)
logger.info(f"Выбрана высота: {height} юнитов")
self._select_combobox("Глубина (мм)", depth)
logger.info(f"Выбрана глубина: {depth} мм")
# Заполняем опциональные поля
if serial:
serial_field = self.page.locator(RackLocators.RACK_SERIAL_FIELD)
serial_field.fill(serial)
logger.info(f"Заполнен серийный номер: {serial}")
if inventory:
inventory_field = self.page.locator(RackLocators.RACK_INVENTORY_FIELD)
inventory_field.fill(inventory)
logger.info(f"Заполнен инвентарный номер: {inventory}")
if comment:
comment_field = self.page.locator(RackLocators.RACK_COMMENT_FIELD)
comment_field.fill(comment)
logger.info(f"Добавлен комментарий: {comment}")
# Заполняем дополнительные combobox поля
if cable_entry:
self._select_combobox("Ввод кабеля", cable_entry)
logger.info(f"Выбран ввод кабеля: {cable_entry}")
if state:
self._select_combobox("Состояние", state)
logger.info(f"Выбрано состояние: {state}")
if owner:
self._select_combobox("Владелец", owner)
logger.info(f"Выбран владелец: {owner}")
if service_org:
self._select_combobox("Обслуживающая организация", service_org)
logger.info(f"Выбрана обслуживающая организация: {service_org}")
if project:
self._select_combobox("Проект/Титул", project)
logger.info(f"Выбран проект/титул: {project}")
logger.info("Данные стойки заполнены")
def _select_combobox(self, field_name: str, value: str) -> None:
"""
Выбор значения в combobox.
Args:
field_name: Название поля
value: Значение для выбора
"""
logger.info(f"Выбор '{value}' в поле '{field_name}'...")
# Получаем статический локатор из словаря
if field_name not in COMBOBOX_FIELDS_MAP:
raise ValueError(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP")
field_locator = COMBOBOX_FIELDS_MAP[field_name]
# Для всех полей используем first() чтобы избежать strict mode violation
field_container = self.page.locator(field_locator).first
# Прокручиваем до поля
field_container.scroll_into_view_if_needed()
self.wait_for_timeout(500)
# Проверяем видимость поля
self.base_component.check_visibility(field_container, f"Поле '{field_name}' не найдено")
# Универсальный клик с force=True для всех полей
field_container.click(force=True)
self.wait_for_timeout(1000)
# Вводим значение
self.page.keyboard.type(value)
self.wait_for_timeout(500)
self.page.keyboard.press("Enter")
logger.info(f"Поле '{field_name}' заполнено")
def create_rack(self, rack_name: str, **kwargs) -> None:
"""
Полный процесс создания стойки.
Args:
rack_name: Наименование стойки
**kwargs: Дополнительные параметры стойки
"""
logger.info(f"Начало процесса создания стойки: {rack_name}")
# Выбираем класс объекта "Стойка"
self.select_object_class("Стойка")
self.wait_for_timeout(1000)
# Проверяем наличие полей стойки
self.check_rack_fields_presence()
# Заполняем данные
self.fill_rack_data(rack_name, **kwargs)
# Создаем стойку
self.click_add_button()
logger.info(f"Процесс создания стойки '{rack_name}' завершен")
def clear_combobox_field(self, field_name: str) -> None:
"""
Очищает значение в combobox поле с помощью кнопки закрытия (крестика).
Args:
field_name: Название поля для очистки
"""
logger.info(f"Очистка combobox поля '{field_name}' с помощью кнопки закрытия...")
if field_name not in COMBOBOX_FIELDS_MAP:
logger.warning(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP")
return
field_locator = COMBOBOX_FIELDS_MAP[field_name]
# Находим поле по локатору
field_container = self.page.locator(field_locator).first
# Проверяем что поле видимо
if not field_container.is_visible():
logger.info(f"Поле '{field_name}' не видимо, пропускаем очистку")
return
# Прокручиваем до поля
field_container.scroll_into_view_if_needed()
self.wait_for_timeout(500)
# Ищем кнопку закрытия (крестик) внутри контейнера поля
close_button = field_container.locator(ComboboxLocators.COMBOBOX_CLOSE_BUTTON)
# Проверяем наличие и видимость кнопки закрытия
if close_button.count() > 0 and close_button.is_visible():
# Если кнопка закрытия видима - кликаем на нее
close_button.click()
self.wait_for_timeout(500)
logger.info(f"Combobox поле '{field_name}' очищено с помощью кнопки закрытия")
else:
# Если кнопки закрытия нет, просто логируем этот факт
logger.info(f"Кнопка закрытия не найдена для поля '{field_name}', очистка не выполнена")
def clear_rack_fields(self) -> None:
"""
Очищает все поля формы создания стойки.
"""
logger.info("Очистка всех полей формы стойки...")
# Очищаем текстовые поля
text_fields = [
(RackLocators.RACK_NAME_FIELD, "Имя"),
(RackLocators.RACK_SERIAL_FIELD, "Серийный номер"),
(RackLocators.RACK_INVENTORY_FIELD, "Инвентарный номер"),
(RackLocators.RACK_COMMENT_FIELD, "Комментарий")
]
for field_locator, field_name in text_fields:
field = self.page.locator(field_locator)
if field.count() > 0 and field.first.is_visible():
field.fill("")
logger.info(f"Текстовое поле '{field_name}' очищено")
# Очищаем combobox поля
combobox_fields = [
"Высота в юнитах",
"Глубина (мм)",
"Ввод кабеля",
"Состояние",
"Владелец",
"Обслуживающая организация",
"Проект/Титул"
]
for field_name in combobox_fields:
self.clear_combobox_field(field_name)
logger.info("Все поля формы стойки очищены")
# =============== МЕТОДЫ ПРОВЕРОК ========================
def check_rack_exists(self, rack_name: str) -> bool:
"""
Проверяет, существует ли уже стойка с указанным именем в навигационной панели.
Args:
rack_name: Имя стойки для проверки
Returns:
bool: True если стойка существует, False если нет
"""
logger.info(f"Проверка существования стойки с именем '{rack_name}'")
self.main_page.click_main_navigation_panel_item("Объекты")
self.main_page.click_main_navigation_panel_item("Объекты")
self.wait_for_timeout(1000)
self.main_page.click_subpanel_item("test-zone")
self.wait_for_timeout(3000)
nav_panel_locator = NavigationPanelLocators.TREEVIEW
# Проверяем видимость элемента через is_visible
element = self.page.locator(nav_panel_locator).get_by_text(rack_name).first
if element.is_visible():
logger.info(f"Стойка с именем '{rack_name}' найдена")
return True
else:
logger.info(f"Стойки с именем '{rack_name}' не найдена")
return False
def should_be_toolbar_buttons(self) -> None:
"""
Проверяет наличие и функциональность кнопок тулбара.
Raises:
AssertionError: Если кнопки недоступны или подсказки неверны.
"""
self.wait_for_timeout(2000)
self.toolbar.check_button_visibility("add")
self.toolbar.check_button_tooltip("add", "Добавить")
self.toolbar.check_button_visibility("cancel")
self.toolbar.check_button_tooltip("cancel", "Отменить")
self.toolbar.click_button("cancel")
self.wait_for_timeout(2000)
def check_toolbar_title(self, expected_title: str) -> None:
"""
Проверяет заголовок тулбара.
Args:
expected_title: Ожидаемый заголовок тулбара
Raises:
AssertionError: Если заголовок не соответствует ожидаемому
"""
logger.info(f"Проверка заголовок тулбара: '{expected_title}'...")
# Используем метод тулбара с фильтрацией по тексту
actual_text = self.toolbar.get_toolbar_title_text(
filter_text="Создать дочерний элемент в"
)
assert expected_title in actual_text, f"Заголовок не совпадает. Ожидалось: '{expected_title}', Получено: '{actual_text}'"
logger.info(f"Заголовок тулбара корректен: '{actual_text}'")
def check_object_class_combobox_presence(self) -> None:
"""
Проверяет наличие combobox 'Класс объекта учета'.
Raises:
AssertionError: Если combobox не найден
"""
logger.info("Проверка наличия combobox 'Класс объекта учета'...")
self.base_component.check_visibility(ComboboxLocators.OBJECT_CLASS_COMBOBOX, "Combobox 'Класс объекта учета' не найден")
logger.info("Combobox 'Класс объекта учета' найден")
def check_object_class_combobox_content(self) -> None:
"""
Проверяет содержимое combobox 'Класс объекта учета'.
Raises:
AssertionError: Если содержимое не соответствует ожидаемому
"""
logger.info("Проверка содержимого combobox 'Класс объекта учета'...")
combobox_locator = self.page.locator(ComboboxLocators.OBJECT_CLASS_COMBOBOX)
# Проверяем что combobox видим
self.base_component.check_visibility(ComboboxLocators.OBJECT_CLASS_COMBOBOX, "Combobox 'Класс объекта учета' не виден")
# Проверяем наличие label
label_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_LABEL)
expect(label_locator).to_have_text("Класс объекта учета")
# Проверяем наличие input поля
input_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_INPUT)
self.base_component.check_visibility(input_locator, "Input поле combobox не найдено")
# Для combobox нормально иметь readonly атрибут - это стандартное поведение
# Проверяем что поле доступно для выбора (не disabled)
expect(input_locator).not_to_have_attribute("disabled", "disabled")
# Проверяем наличие иконки стрелки
icon_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_ICON_ARROW)
self.base_component.check_visibility(icon_locator, "Иконка стрелки combobox не найдена")
logger.info("Содержимое combobox 'Класс объекта учета' корректно")
def check_object_class_selected(self, expected_class: str) -> None:
"""
Проверяет что выбран указанный класс объекта.
Args:
expected_class: Ожидаемый выбранный класс объекта
Raises:
AssertionError: Если выбранный класс не соответствует ожидаемому
"""
logger.info(f"Проверка выбранного класса объекта: '{expected_class}'...")
# Даем время на обновление значения
self.wait_for_timeout(1000)
actual_class = self.get_selected_object_class()
# Проверка - допускаем частичное совпадение
if expected_class.lower() in actual_class.lower() or actual_class.lower() in expected_class.lower():
logger.info(f"Класс объекта '{expected_class}' успешно выбран (фактически: '{actual_class}')")
else:
raise AssertionError(f"Выбранный класс не соответствует ожидаемому. Ожидалось: '{expected_class}', Получено: '{actual_class}'")
def check_object_class_options_content(self, expected_options: list = None) -> None:
"""
Проверяет содержимое списка опций combobox.
Args:
expected_options: Ожидаемый список опций. Если None, проверяет только что список не пустой.
Raises:
AssertionError: Если список опций не соответствует ожидаемому
"""
logger.info("Проверка содержимого списка опций combobox...")
# Получаем доступные опции
available_options = self.get_object_class_options()
if expected_options is not None:
# Проверяем соответствие ожидаемому списку
assert set(available_options) == set(expected_options), (
f"Список опций не соответствует ожидаемому. "
f"Ожидалось: {expected_options}, Получено: {available_options}"
)
else:
# Проверяем что список не пустой
assert len(available_options) > 0, "Список опций combobox пустой"
logger.info(f"Содержимое списка опций корректно: {available_options}")
def check_dropdown_item_presence(self, item_text: str) -> None:
"""
Проверяет наличие элемента в выпадающем списке.
Args:
item_text: Текст элемента для проверки
"""
logger.info(f"Проверка наличия элемента '{item_text}' в выпадающем списке...")
# Получаем все опции и проверяем наличие
available_options = self.get_object_class_options()
if item_text not in available_options:
raise AssertionError(f"Элемент '{item_text}' не найден в списке опций. Доступные опции: {available_options}")
logger.info(f"Элемент '{item_text}' присутствует в списке")
def check_rack_fields_presence(self) -> None:
"""
Проверяет наличие полей специфичных для стойки.
Raises:
AssertionError: Если какое-либо поле не найдено
"""
logger.info("Проверка наличия полей для стойки...")
# Основные обязательные поля
required_fields = [
(RackLocators.RACK_NAME_FIELD, "Имя"),
(RackLocators.RACK_HEIGHT_FIELD, "Высота в юнитах"),
(RackLocators.RACK_DEPTH_FIELD, "Глубина (мм)")
]
# Дополнительные поля
optional_fields = [
(RackLocators.RACK_SERIAL_FIELD, "Серийный номер"),
(RackLocators.RACK_INVENTORY_FIELD, "Инвентарный номер"),
(RackLocators.RACK_COMMENT_FIELD, "Комментарий"),
(RackLocators.RACK_CABLE_ENTRY_FIELD, "Ввод кабеля"),
(RackLocators.RACK_STATE_FIELD, "Состояние"),
(RackLocators.RACK_OWNER_FIELD, "Владелец"),
(RackLocators.RACK_SERVICE_ORG_FIELD, "Обслуживающая организация"),
(RackLocators.RACK_PROJECT_FIELD, "Проект/Титул")
]
# Проверяем обязательные поля
for field_locator, field_name in required_fields:
self.base_component.check_visibility(field_locator, f"Обязательное поле '{field_name}' не найдено")
logger.info(f"Обязательное поле '{field_name}' найдено")
# Проверяем дополнительные поля
for field_locator, field_name in optional_fields:
field = self.page.locator(field_locator)
if field.count() > 0 and field.first.is_visible():
logger.info(f"Дополнительное поле '{field_name}' найдено")
else:
logger.info(f"Дополнительное поле '{field_name}' не найдено или не отображается")
logger.info("Все основные поля для стойки присутствуют")
def check_field_highlighted_error(self, field_name: str) -> None:
"""
Проверяет, что поле подсвечено цветом ошибки (валидация не пройдена).
Args:
field_name: Название поля для проверки
"""
logger.info(f"Проверка подсветки поля '{field_name}' цветом ошибки...")
# Локаторы только для обязательных полей
required_fields = {
"Имя": RackLocators.RACK_NAME_FIELD,
"Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD,
"Глубина (мм)": RackLocators.RACK_DEPTH_FIELD
}
if field_name not in required_fields:
raise ValueError(f"Поле '{field_name}' не является обязательным или не поддерживается")
field_locator = required_fields[field_name]
field_element = self.page.locator(field_locator)
# Проверяем что поле видимо
self.base_component.check_visibility(field_element, f"Поле '{field_name}' не найдено")
# Ищем родительский контейнер с использованием константы
parent_container = field_element.locator(RackLocators.INPUT_PARENT_CONTAINER).first
# Проверка классов ошибки
if parent_container.count() > 0:
error_classes = AlertLocators.ERROR_CLASSES
is_error_highlighted = False
for error_class in error_classes:
error_element = parent_container.locator(f".{error_class}")
if error_element.count() > 0:
is_error_highlighted = True
logger.info(f"Поле '{field_name}' подсвечено ошибкой")
break
if not is_error_highlighted:
raise AssertionError(f"Поле '{field_name}' не подсвечено цветом ошибки ")
logger.info(f"Поле '{field_name}' корректно подсвечено цветом ошибки")
def check_field_not_highlighted_error(self, field_name: str) -> None:
"""
Проверяет, что поле НЕ подсвечено цветом ошибки (валидация успешна).
Args:
field_name: Название поля для проверки
"""
logger.info(f"Проверка отсутствия подсветки ошибки у поля '{field_name}'...")
# Локаторы только для обязательных полей
required_fields = {
"Имя": RackLocators.RACK_NAME_FIELD,
"Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD,
"Глубина (мм)": RackLocators.RACK_DEPTH_FIELD
}
if field_name not in required_fields:
raise ValueError(f"Поле '{field_name}' не является обязательным или не поддерживается")
field_locator = required_fields[field_name]
field_element = self.page.locator(field_locator)
# Проверяем что поле видимо
self.base_component.check_visibility(field_element, f"Поле '{field_name}' не найдено")
# Ищем родительский контейнер с использованием константы
parent_container = field_element.locator(RackLocators.INPUT_PARENT_CONTAINER).first
# Поверка отсутствия классов ошибки
if parent_container.count() > 0:
error_classes = AlertLocators.ERROR_CLASSES
for error_class in error_classes:
error_element = parent_container.locator(f".{error_class}")
if error_element.count() > 0:
raise AssertionError(f"Поле '{field_name}' подсвечено ошибкой")
logger.info(f"Поле '{field_name}' корректно не подсвечено цветом ошибки")

View File

@ -103,6 +103,16 @@ class MainPage(BasePage):
node_locator, item_name, parent 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: def click_user_button(self) -> UserCard:
"""Выполняет нажатие кнопки пользователя.""" """Выполняет нажатие кнопки пользователя."""
@ -207,6 +217,25 @@ class MainPage(BasePage):
item_name 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: def check_navigation_panel_verticall_scrolling(self) -> bool:
"""Проверяет возможность вертикальной прокрутки панели. """Проверяет возможность вертикальной прокрутки панели.
@ -233,3 +262,13 @@ class MainPage(BasePage):
NavigationPanelLocators.PANEL_MAIN, NavigationPanelLocators.PANEL_MAIN,
"Navigation panel is missing" "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()

View File

@ -48,103 +48,27 @@ class RackPage(BasePage):
show_button_locator = self.page.locator(RackLocators.SHOW_RACK_BUTTON) show_button_locator = self.page.locator(RackLocators.SHOW_RACK_BUTTON)
self.show_button = TooltipButton(page, show_button_locator, "show_rack") self.show_button = TooltipButton(page, show_button_locator, "show_rack")
# Кнопка "Переместить"
replace_button_locator = self.page.locator(RackLocators.TOOLBAR_REPLACE_BUTTON)
self.replace_button = TooltipButton(page, replace_button_locator, "replace")
# Кнопка "Сохранить"
done_button_locator = self.page.locator(RackLocators.TOOLBAR_DONE_BUTTON)
self.done_button = TooltipButton(page, done_button_locator, "done")
# Кнопка "Отменить"
close_button_locator = self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON)
self.close_button = TooltipButton(page, close_button_locator, "close")
# Кнопка "Удалить"
remove_button_locator = self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON)
self.remove_button = TooltipButton(page, remove_button_locator, "remove")
self.toolbar = ToolbarComponent(page, "") self.toolbar = ToolbarComponent(page, "")
self.toolbar.add_tooltip_button(locator_button, "edit") self.toolbar.add_tooltip_button(locator_button, "edit")
self.toolbar.add_tooltip_button(hide_button_locator, "hide_rack") self.toolbar.add_tooltip_button(hide_button_locator, "hide_rack")
self.toolbar.add_tooltip_button(show_button_locator, "show_rack") self.toolbar.add_tooltip_button(show_button_locator, "show_rack")
self.toolbar.add_tooltip_button(replace_button_locator, "replace")
self.toolbar.add_tooltip_button(done_button_locator, "done")
self.toolbar.add_tooltip_button(close_button_locator, "close")
self.toolbar.add_tooltip_button(remove_button_locator, "remove")
# Действия # Действия
def click_remove_button(self) -> None: def click_edit_button(self) -> None:
""" """
Кликает на кнопку 'Удалить' и обрабатывает диалог подтверждения. Кликает на кнопку 'Изменить'.
""" """
logger.debug("Clicking on 'Remove' button...") logger.debug("Clicking on 'Edit' button...")
# Проверяем видимость кнопки # Проверяем видимость кнопки
self.toolbar.check_button_visibility("remove") self.toolbar.check_button_visibility("edit")
self.toolbar.check_button_tooltip("edit", "Изменить")
# Проверяем тултип кнопки (может быть "Удалить" или "Remove")
try:
self.toolbar.check_button_tooltip("remove", "Удалить")
except AssertionError:
try:
self.toolbar.check_button_tooltip("remove", "Remove")
except AssertionError:
logger.debug("Could not verify tooltip text for remove button")
# Кликаем на кнопку удаления
self.toolbar.get_button_by_name("remove").click()
self.wait_for_timeout(1000)
# Ожидаем появления диалога подтверждения
self._handle_remove_confirmation_dialog()
def confirm_remove_dialog(self, confirm: bool = True) -> None:
"""
Подтверждает или отклоняет удаление в диалоговом окне.
Args:
confirm (bool): Если True - подтвердить удаление, если False - отменить
"""
logger.debug(f"Confirming remove dialog with: {'Да' if confirm else 'Нет'}")
# Ждем немного перед поиском диалога
self.wait_for_timeout(1500)
# Ищем активный диалог
dialog = self.page.locator("div.v-dialog--active")
# Проверяем, что диалог найден и содержит нужный текст
assert dialog.count() > 0, "No active dialog found"
# Проверяем текст диалога
dialog_text = dialog.first.text_content()
logger.debug("Dialog text: %s", dialog_text)
# Должен содержать "Запрос подтверждения" и "Удалить"
assert "Запрос подтверждения" in dialog_text, "Not a confirmation dialog"
# Ищем кнопку по data-testid
if confirm:
button = self.page.locator(RackLocators.CONFIRM_REMOVE_YES_BUTTON)
else:
button = self.page.locator(RackLocators.CONFIRM_REMOVE_NO_BUTTON)
# Проверяем, что кнопка найдена
assert button.count() > 0, "Button not found with selector"
# Кликаем на кнопку # Кликаем на кнопку
button_text = button.first.text_content() self.toolbar.get_button_by_name("edit").click()
logger.debug("Clicking button with text: %s", button_text)
button.first.click()
self.wait_for_timeout(2000)
# Проверяем, что диалог закрылся
self.wait_for_timeout(1000) self.wait_for_timeout(1000)
logger.debug("Remove confirmation completed")
def get_available_tabs(self) -> list[str]: def get_available_tabs(self) -> list[str]:
""" """
Возвращает список доступных вкладок. Возвращает список доступных вкладок.
@ -536,18 +460,6 @@ class RackPage(BasePage):
# Кликаем на кнопку "Изменить" для проверки функциональности # Кликаем на кнопку "Изменить" для проверки функциональности
self.toolbar.get_button_by_name("edit").click() self.toolbar.get_button_by_name("edit").click()
# Проверяем новые кнопки тулбара
self.toolbar.check_button_visibility("replace")
self.toolbar.check_button_tooltip("replace", "Переместить")
self.toolbar.check_button_visibility("done")
self.toolbar.check_button_tooltip("done", "Сохранить")
self.toolbar.check_button_visibility("close")
self.toolbar.check_button_tooltip("close", "Отменить")
self.toolbar.check_button_visibility("remove")
self.toolbar.check_button_tooltip("remove", "Удалить")
def should_have_hide_rack_button(self) -> None: def should_have_hide_rack_button(self) -> None:
""" """
@ -690,11 +602,6 @@ class RackPage(BasePage):
logger.debug("%s check completed successfully", side_name) logger.debug("%s check completed successfully", side_name)
def _handle_remove_confirmation_dialog(self) -> None:
"""Обрабатывает диалог подтверждения удаления."""
logger.debug("Handling remove confirmation dialog...")
self.confirm_remove_dialog(confirm=True)
def _wait_for_tab_activation(self, tab_name: str, timeout: int = 5000) -> None: def _wait_for_tab_activation(self, tab_name: str, timeout: int = 5000) -> None:
""" """
Ожидает активации вкладки. Ожидает активации вкладки.

View File

@ -1,466 +0,0 @@
"""Модуль тестов вкладки 'Стойка'.
Содержит тесты для проверки функциональности
работы со стойкой оборудования.
"""
from playwright.sync_api import Page, expect
from elements.tooltip_button_element import TooltipButton
from components.toolbar_component import ToolbarComponent
from pages.base_page import BasePage
from locators.rack_locators import RackLocators
from tools.logger import get_logger
logger = get_logger("RACK_TAB")
# Специфичные локаторы оставленые в основном коде
PANEL_HEADER = "//span[text()='Объекты']/following-sibling::i"
TOOLBAR_CONTENT = "//div[@class='v-toolbar__content']"
EDIT_BUTTON_ANCESTOR_DIV3 = "xpath=/ancestor::div[3]//button"
PANEL_HEADER_ANCESTOR_DIV2 = "xpath=/ancestor::div[2]"
class RackTab(BasePage):
"""Класс для работы с вкладкой стойки оборудования."""
def __init__(self, page: Page) -> None:
"""
Инициализирует объект вкладки стойки.
Args:
page: Экземпляр страницы Playwright
"""
super().__init__(page)
locator_button = self.page.locator(PANEL_HEADER).\
locator(EDIT_BUTTON_ANCESTOR_DIV3).nth(0)
self.edit_button = TooltipButton(page, locator_button, "edit")
self.toolbar = ToolbarComponent(page, "")
self.toolbar.add_tooltip_button(locator_button, "edit")
def wait_for_rack_loading(self, timeout: int = 15000) -> None:
"""
Ожидает загрузки интерфейса стойки.
Args:
timeout: Время ожидания в миллисекундах (по умолчанию 15000)
Raises:
TimeoutError: Если загрузка не завершилась в указанное время
"""
logger.info("Ожидание загрузки интерфейса стойки...")
# Ждем появления основного контейнера
main_container = self.page.locator(RackLocators.MAIN_CONTAINER)
expect(main_container).to_be_visible(timeout=timeout)
# Ждем появления юнитов
units = self.page.locator(RackLocators.ALL_UNITS)
expect(units).to_have_count(20, timeout=timeout)
logger.info("Интерфейс стойки загружен")
def get_toolbar_title(self) -> list[str]:
"""
Получает заголовок панели инструментов.
Returns:
list[str]: Список элементов заголовка панели инструментов
"""
toolbar_title_locator = self.page.locator(PANEL_HEADER).\
locator(PANEL_HEADER_ANCESTOR_DIV2).get_by_role("navigation").\
locator(TOOLBAR_CONTENT)
return self.toolbar.get_toolbar_composite_title_text(toolbar_title_locator)
def switch_to_tab(self, tab_name: str) -> None:
"""
Переключается на указанную вкладку.
Args:
tab_name: Название вкладки для переключения
Raises:
AssertionError: Если вкладка не найдена или недоступна
"""
logger.info(f"Переключение на вкладку '{tab_name}'...")
tab = self.page.locator(RackLocators.TAB_BY_NAME.format(tab_name))
if tab.count() == 0:
raise AssertionError(f"Вкладка '{tab_name}' не найдена")
# Проверяем активность ДО клика
if self.is_tab_active(tab_name):
logger.info(f"Вкладка '{tab_name}' уже активна")
return
# Находим первую видимую вкладку с нужным именем
target_tab = None
for i in range(tab.count()):
element = tab.nth(i)
if element.is_visible() and element.is_enabled():
target_tab = element
break
if not target_tab:
raise AssertionError(f"Не найдена видимая/доступная вкладка '{tab_name}'")
# Кликаем на вкладку
logger.info(f"Клик на вкладку '{tab_name}'...")
target_tab.click()
# Ждем изменения активной вкладки
self._wait_for_tab_activation(tab_name)
# Ждем загрузки контента
self.page.wait_for_timeout(500)
def switch_to_general_info_tab(self) -> None:
"""Переключается на вкладку 'Общая информация'."""
self.switch_to_tab("Общая информация")
def switch_to_maintenance_tab(self) -> None:
"""Переключается на вкладку 'Обслуживание'."""
self.switch_to_tab("Обслуживание")
def switch_to_events_tab(self) -> None:
"""Переключается на вкладку 'События'."""
self.switch_to_tab("События")
def switch_to_services_tab(self) -> None:
"""Переключается на вкладку 'Сервисы'."""
self.switch_to_tab("Сервисы")
def is_tab_active(self, tab_name: str) -> bool:
"""
Проверяет, активна ли указанная вкладка.
Args:
tab_name: Название вкладки для проверки
Returns:
bool: True если вкладка активна, False в противном случае
"""
# Метод 1: Проверяем по активному классу и тексту, метод быстый, если надо универсальный оставояем метод 2 - медленный
active_tab = self.page.locator(RackLocators.ACTIVE_TAB)
if active_tab.count() > 0 and active_tab.first.is_visible():
active_text = active_tab.first.text_content()
if active_text and active_text.strip() == tab_name:
logger.info(f"Вкладка '{tab_name}' активна (через класс активной вкладки)")
return True
# Метод 2: Проверяем по классам у конкретной вкладки
tab = self.page.locator(RackLocators.TAB_BY_NAME.format(tab_name))
if tab.count() > 0:
for i in range(tab.count()):
element = tab.nth(i)
if element.is_visible() and element.is_enabled():
element_class = element.get_attribute("class") or ""
is_active = any(
active_class in element_class
for active_class in RackLocators.ACTIVE_TAB_CLASSES
)
if is_active:
logger.info(f"Вкладка '{tab_name}' активна (классы: {element_class})")
return True
logger.info(f"Вкладка '{tab_name}' не активна")
return False
def get_available_tabs(self) -> list[str]:
"""
Возвращает список доступных вкладок используя DOM структуру.
Returns:
list[str]: Список названий доступных вкладок
"""
tabs = []
# Используем локатор для верхних вкладок
tab_elements = self.page.locator(RackLocators.ALL_TABS)
# Ждем появления элементов
tab_elements.first.wait_for(state="visible", timeout=5000)
total_count = tab_elements.count()
logger.info(f"Всего найдено элементов верхних вкладок: {total_count}")
for i in range(total_count):
element = tab_elements.nth(i)
# Проверяем видимость и доступность элемента
if element.is_visible() and element.is_enabled():
tab_text = element.text_content()
if tab_text:
tab_text = tab_text.strip()
if tab_text and tab_text not in tabs:
tabs.append(tab_text)
logger.info(f"Найдена верхняя вкладка: '{tab_text}'")
logger.info(f"Найдены доступные верхние вкладки: {tabs}")
return tabs
def _wait_for_tab_activation(self, tab_name: str, timeout: int = 5000) -> None:
"""
Ожидает активации вкладки.
Args:
tab_name: Название вкладки для ожидания
timeout: Время ожидания в миллисекундах
Raises:
AssertionError: Если вкладка не активирована в течение таймаута
"""
logger.info(f"Ожидание активации вкладки '{tab_name}'...")
start_time = self.page.evaluate("Date.now()")
while self.page.evaluate("Date.now()") - start_time < timeout:
if self.is_tab_active(tab_name):
logger.info(f"Вкладка '{tab_name}' успешно активирована")
return
self.page.wait_for_timeout(100)
raise AssertionError(f"Вкладка '{tab_name}' не активирована в течение {timeout}мс")
def should_be_toolbar_buttons(self) -> None:
"""
Проверяет наличие и функциональность кнопок тулбара.
Raises:
AssertionError: Если кнопки недоступны или подсказки неверны.
"""
logger.info("Проверка кнопок панели инструментов...")
self.toolbar.check_button_visibility("edit")
self.toolbar.check_button_tooltip("edit", "Изменить")
self.toolbar.get_button_by_name("edit").click()
def should_be_rack_sides_displayed(self) -> None:
"""
Проверка отображения и структуры сторон стойки.
Raises:
AssertionError: Если стороны стойки не отображаются корректно
"""
logger.info("Проверка отображения и структуры сторон стойки...")
# Ожидаем загрузки
self.wait_for_rack_loading()
# БАЗОВАЯ ПРОВЕРКА: обе стороны отображаются
logger.info("--- Базовая проверка отображения сторон ---")
front_side_section = self.page.locator(RackLocators.FRONT_SIDE_SECTION).first
expect(front_side_section).to_be_visible(timeout=10000)
logger.info("Секция лицевой стороны найдена")
back_side_section = self.page.locator(RackLocators.BACK_SIDE_SECTION).first
expect(back_side_section).to_be_visible(timeout=10000)
logger.info("Секция обратной стороны найдена")
# Проверяем заголовки
front_side_title = front_side_section.locator(RackLocators.FRONT_SIDE_TITLE)
expect(front_side_title).to_be_visible(timeout=5000), "Заголовок 'Лицевая сторона' не отображается"
logger.info("Заголовок 'Лицевая сторона' отображается")
back_side_title = back_side_section.locator(RackLocators.BACK_SIDE_TITLE)
expect(back_side_title).to_be_visible(timeout=5000), "Заголовок 'Обратная сторона' не отображается"
logger.info("Заголовок 'Обратная сторона' отображается")
# Проверяем позиции юнитов
unit_positions = self.page.locator(RackLocators.UNIT_POSITIONS)
total_positions = unit_positions.count()
logger.info(f"Всего позиций юнитов: {total_positions}")
assert total_positions > 0, "Не найдено позиций юнитов"
# Детальная проверка лицевой стороны
logger.info("--- Детальная проверка лицевой стороны ---")
self._check_front_side_details(front_side_section)
# Детальная проверка обратной стороны
logger.info("--- Детальная проверка обратной стороны ---")
self._check_back_side_details(back_side_section)
logger.info("Все проверки сторон стойки пройдены успешно")
def _check_front_side_details(self, front_side_section) -> None:
"""
Проверка структуры лицевой стороны стойки.
Args:
front_side_section: Локатор секции лицевой стороны
Raises:
AssertionError: Если структура лицевой стороны некорректна
"""
# Проверяем юниты в секции лицевой стороны
front_side_units = front_side_section.locator(RackLocators.FRONT_SIDE_UNITS)
unit_count = front_side_units.count()
logger.info(f"Найдено юнитов на лицевой стороне: {unit_count}")
assert unit_count >= 1, f"Не найдено юнитов на лицевой стороне. Ожидалось минимум 1, найдено {unit_count}"
# Проверяем наличие устройств на лицевой стороне
front_side_devices = front_side_section.locator(RackLocators.FRONT_SIDE_DEVICES)
device_count = front_side_devices.count()
logger.info(f"Найдено физических устройств на лицевой стороне: {device_count}")
if device_count > 0:
for i in range(device_count):
device = front_side_devices.nth(i)
device_title = device.get_attribute("title")
device_classes = device.get_attribute("class") or ""
logger.info(f" Устройство {i}: title='{device_title}', classes='{device_classes}'")
def _check_back_side_details(self, back_side_section) -> None:
"""
Проверка структуры обратной стороны стойки.
Args:
back_side_section: Локатор секции обратной стороны
Raises:
AssertionError: Если структура обратной стороны некорректна
"""
# Проверяем юниты в секции обратной стороны
back_side_units = back_side_section.locator(RackLocators.BACK_SIDE_UNITS)
unit_count = back_side_units.count()
logger.info(f"Найдено юнитов на обратной стороне: {unit_count}")
assert unit_count >= 1, f"Не найдено юнитов на обратной стороне. Ожидалось минимум 1, найдено {unit_count}"
# Проверяем наличие устройств на обратной стороне
back_side_devices = back_side_section.locator(RackLocators.BACK_SIDE_DEVICES)
device_count = back_side_devices.count()
logger.info(f"Найдено физических устройств на обратной стороне: {device_count}")
if device_count > 0:
for i in range(device_count):
device = back_side_devices.nth(i)
device_title = device.get_attribute("title")
device_classes = device.get_attribute("class") or ""
logger.info(f" Устройство {i}: title='{device_title}', classes='{device_classes}'")
def should_be_header_panel(self, expected_toolbar_title_items: list[str]) -> None:
"""
Проверяет наличие и корректность заголовка панели.
Args:
expected_toolbar_title_items: Ожидаемые элементы заголовка
Raises:
AssertionError: Если заголовок панели не соответствует ожиданиям
"""
panel_header_locator = self.page.locator(PANEL_HEADER)
expect(panel_header_locator).to_be_visible(), "Panel header 'Объекты'"
if panel_header_locator.inner_text() != 'chevron_right':
assert False, "No separator 'chevron_right' after header 'Объекты'"
actual_toolbar_title_items = self.get_toolbar_title()
self.check_lists_equals(actual_toolbar_title_items,
expected_toolbar_title_items,
f"Miscomparison actual {actual_toolbar_title_items} and expected {expected_toolbar_title_items}")
self.toolbar.check_button_visibility("edit")
def check_tab_switching(self) -> None:
"""
Проверяет переключение между вкладками стойки в соответствии с локаторами.
Raises:
AssertionError: Если переключение на одну или более вкладок не удалось
"""
logger.info("Тестирование функциональности переключения вкладок стойки...")
# Вкладки
defined_tabs = [
"Общая информация",
"Обслуживание",
"События",
"Сервисы"
]
logger.info(f"Тестируемые определенные вкладки: {defined_tabs}")
successful_switches = 0
failed_switches = []
# Тестируем переключение на каждую определенную вкладку
for tab_name in defined_tabs:
logger.info(f"Тестирование переключения на вкладку '{tab_name}'...")
# Проверяем существование локатора для этой вкладки
tab_locator = RackLocators.TAB_BY_NAME.format(tab_name)
tab_elements = self.page.locator(tab_locator)
# Проверяем наличие элементов через count()
if tab_elements.count() == 0:
logger.warning(f"Вкладка '{tab_name}' не найдена на странице")
failed_switches.append(f"Вкладка '{tab_name}' не найдена")
continue
# Находим видимую и доступную вкладку
target_tab = None
for i in range(tab_elements.count()):
element = tab_elements.nth(i)
# Проверки видимости и доступности
if element.is_visible() and element.is_enabled():
target_tab = element
break
if not target_tab:
logger.warning(f"Не найдена видимая/доступная вкладка '{tab_name}'")
failed_switches.append(f"Вкладка '{tab_name}' не видима/не доступна")
continue
# Переключаемся на вкладку
logger.info(f"Переключение на вкладку '{tab_name}'...")
# Проверяем активность ДО клика
if self.is_tab_active(tab_name):
logger.info(f"Вкладка '{tab_name}' уже активна")
successful_switches += 1
continue
# Кликаем на вкладку с таймаутом
target_tab.click(timeout=5000)
# Ждем изменения активной вкладки
self._wait_for_tab_activation(tab_name)
# Проверяем, что вкладка активна
if not self.is_tab_active(tab_name):
logger.warning(f"Вкладка '{tab_name}' не активна после переключения")
failed_switches.append(f"Вкладка '{tab_name}' не активна после клика")
continue
logger.info(f"Успешно переключено на вкладку '{tab_name}'")
successful_switches += 1
# Небольшая пауза между переключениями для стабильности
self.page.wait_for_timeout(1000)
# Формируем итоговый отчет
logger.info("=== РЕЗУЛЬТАТЫ ПЕРЕКЛЮЧЕНИЯ ВКЛАДОК ===")
logger.info(f"Успешных переключений: {successful_switches}/{len(defined_tabs)}")
if failed_switches:
logger.info("Неудачные переключения:")
for failure in failed_switches:
logger.info(f" - {failure}")
# Требуем успешного переключения на все определенные вкладки
if successful_switches < len(defined_tabs):
raise AssertionError(
f"Тест переключения вкладок не пройден. "
f"Только {successful_switches} из {len(defined_tabs)} определенных вкладок переключены успешно. "
f"Ошибки: {', '.join(failed_switches)}"
)
logger.info(f"Все {successful_switches} определенных вкладок успешно переключены!")

View File

@ -8,8 +8,7 @@ from playwright.sync_api import Page
from tools.logger import get_logger from tools.logger import get_logger
from locators.table_locators import TableLocators from locators.table_locators import TableLocators
from locators.modal_window_locators import ModalWindowLocators from locators.modal_window_locators import ModalWindowLocators
from components_derived.modal_view_ztp_template import ViewZTPTemplateModalWindow from components_derived.modal_view_template import ViewTemplateModalWindow
from components.modal_window_component import ModalWindowComponent
from components.toolbar_component import ToolbarComponent from components.toolbar_component import ToolbarComponent
from components.table_component import TableComponent from components.table_component import TableComponent
from pages.base_page import BasePage from pages.base_page import BasePage
@ -44,16 +43,16 @@ class ZTPTemplatesTab(BasePage):
Args: Args:
title: Заголовок окна. 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: Args:
title: Заголовок окна. title: Заголовок окна.
Returns: Returns:
ViewZTPTemplateModalWindow: Экземпляр модального окна шаблона. ViewTemplateModalWindow: Экземпляр модального окна шаблона.
Raises: Raises:
AssertionError: Если окно не найдено. AssertionError: Если окно не найдено.
@ -92,14 +91,14 @@ class ZTPTemplatesTab(BasePage):
row_locator.click() row_locator.click()
# Создаем временный экземпляр модального окна для получения заголовка # Создаем временный экземпляр модального окна для получения заголовка
temp_modal = ViewZTPTemplateModalWindow(self.page, "") temp_modal = ViewTemplateModalWindow(self.page, "")
title = temp_modal.toolbar.get_toolbar_title_text( template_name = temp_modal.toolbar.get_toolbar_title_text(
ModalWindowLocators.MODAL_WINDOW_TITLE ModalWindowLocators.MODAL_WINDOW_TITLE
) )
# Добавляем модальное окно в коллекцию после открытия # Добавляем модальное окно в коллекцию после открытия
self.add_modal_window(title) self.add_modal_window(template_name)
return title return template_name
def close_modal_window_by_toolbar_button(self, title: str) -> None: def close_modal_window_by_toolbar_button(self, title: str) -> None:
"""Закрывает модальное окно через кнопку в тулбаре. """Закрывает модальное окно через кнопку в тулбаре.
@ -108,17 +107,7 @@ class ZTPTemplatesTab(BasePage):
title: Заголовок окна. title: Заголовок окна.
""" """
modal_window = self.get_modal_window(title) modal_window = self.get_modal_window(title)
modal_window.close_window_by_toolbar_button() modal_window.click_toolbar_close_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()
self.delete_modal_window(title) self.delete_modal_window(title)
def get_rows_count(self) -> int: def get_rows_count(self) -> int:
@ -142,27 +131,29 @@ class ZTPTemplatesTab(BasePage):
def scroll_modal_up(self) -> None: def scroll_modal_up(self) -> None:
"""Прокручивает содержимое модального окна вверх.""" """Прокручивает содержимое модального окна вверх."""
temp_modal = ModalWindowComponent(self.page) self.ztp_templates_table.scroll_up(
temp_modal.scroll_window_up() ModalWindowLocators.MODAL_WINDOW_SCROLL_CONTAINER
)
def scroll_modal_down(self) -> None: def scroll_modal_down(self) -> None:
"""Прокручивает содержимое модального окна вниз.""" """Прокручивает содержимое модального окна вниз."""
temp_modal = ModalWindowComponent(self.page) self.ztp_templates_table.scroll_down(
temp_modal.scroll_window_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: Args:
title: Имя шаблона для проверки заголовка окна. template_name: Имя шаблона для проверки заголовка окна.
Raises: Raises:
AssertionError: Если элементы окна некорректны. AssertionError: Если элементы окна некорректны.
""" """
modal_window = self.get_modal_window(title) modal_window = self.get_modal_window(template_name)
modal_window.check_content() 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: Returns:
bool: True если скроллинг возможен, иначе False. bool: True если скроллинг возможен, иначе False.
""" """
temp_modal = ModalWindowComponent(self.page) return self.ztp_templates_table.is_scrollable_vertically(
return temp_modal.check_window_vertical_scrolling() 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. """Проверяет соответствие данных модального окна данным из API.
Процесс проверки: Процесс проверки:
@ -279,7 +271,7 @@ class ZTPTemplatesTab(BasePage):
5. Выбрасывает assertion при обнаружении расхождений 5. Выбрасывает assertion при обнаружении расхождений
Args: Args:
title: Имя шаблона для проверки (должно совпадать с id в API). template_name: Имя шаблона для проверки (должно совпадать с id в API).
Raises: Raises:
AssertionError: Если: 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() actual_data = modal_window.get_modal_window_data()
# Читаем данные из API # Читаем данные из API
encoded_title = title.replace(" ", "%20") encoded_template_name = template_name.replace(" ", "%20")
url = f"e-nms/DHCP/showOptPattern?template={encoded_title}" url = f"e-nms/DHCP/showOptPattern?template={encoded_template_name}"
response = self.send_get_api_request(url) response = self.send_get_api_request(url)
# Проверяем статус ответа # Проверяем статус ответа
@ -308,5 +300,5 @@ class ZTPTemplatesTab(BasePage):
response_data = response.json() response_data = response.json()
template_data = response_data['data'] template_data = response_data['data']
# Сравниваем actual_data с данными конкретного шаблона # Сравниваем actual_data с данными конкретного шаблона (метод теперь в ViewTemplateModalWindow)
modal_window.compare_modal_with_api_data(actual_data, template_data, title) modal_window.compare_modal_with_api_data(actual_data, template_data, template_name)

View File

@ -167,10 +167,15 @@ class TestCreateRackElement:
logger.debug("Edit button clicked, waiting for edit form...") logger.debug("Edit button clicked, waiting for edit form...")
# 3. Используем метод click_remove_button, который обрабатывает весь процесс удаления # 3. Создаем экземпляр ModalRackEdit и используем его метод
# включая диалог подтверждения rack_edit = ModalRackEdit(browser)
# Проверяем видимость кнопки удаления в модальном окне
rack_edit.check_toolbar_button_visibility("remove")
# Используем метод из ModalRackEdit для удаления
logger.debug("Clicking remove button...") logger.debug("Clicking remove button...")
rack_page.click_remove_button() rack_edit.click_remove_button()
# 4. Проверяем уведомление об успешном удалении - требуется создать разработчику (заведена задача) # 4. Проверяем уведомление об успешном удалении - требуется создать разработчику (заведена задача)
# Создаем экземпляр фрейма для доступа к alert компоненту # Создаем экземпляр фрейма для доступа к alert компоненту

View File

@ -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()

View File

@ -5,18 +5,25 @@
""" """
import pytest import pytest
from playwright.sync_api import Page import os
from playwright.sync_api import Page, expect
from locators.navigation_panel_locators import NavigationPanelLocators from locators.navigation_panel_locators import NavigationPanelLocators
from pages.location_page import LocationPage
from components_derived.accounting_objects.rack_maker import RackObjectMaker, RackData from components_derived.accounting_objects.rack_maker import RackObjectMaker, RackData
from components_derived.frames.create_child_element_frame import CreateChildElementFrame from components_derived.frames.create_child_element_frame import CreateChildElementFrame
from pages.location_page import LocationPage
from pages.login_page import LoginPage from pages.login_page import LoginPage
from pages.main_page import MainPage from pages.main_page import MainPage
from pages.rack_page import RackPage from pages.rack_page import RackPage
from tools.logger import get_logger
from components_derived.modal_edit_rack import ModalEditRack, RackEditData
# Константы # Константы
RACK_NAME = "Test-Rack-Functionality" RACK_NAME = "Test-Rack-Functionality"
# Инициализация логгера для всего модуля
logger = get_logger("RACK_TESTS")
#logger.setLevel("INFO")
class TestRackTab: class TestRackTab:
"""Набор тестов для вкладки 'Стойка' в модуле Объекты. """Набор тестов для вкладки 'Стойка' в модуле Объекты.
@ -26,6 +33,10 @@ class TestRackTab:
Тесты покрывают следующие функциональные области: Тесты покрывают следующие функциональные области:
1. test_rack_tab_content - Базовая структура и содержимое вкладки стойки 1. test_rack_tab_content - Базовая структура и содержимое вкладки стойки
2. test_rack_tab_switching - Функциональность переключения между вкладками стойки 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 - Навигация по всем вкладкам модального окна редактирования
""" """
# Инициализируем атрибуты # Инициализируем атрибуты
@ -63,6 +74,8 @@ class TestRackTab:
browser: Страница Playwright browser: Страница Playwright
rack_name: Имя стойки для создания rack_name: Имя стойки для создания
""" """
logger.debug(f"Creating rack: {rack_name}")
# Нажимаем кнопку "Создать" на тулбаре # Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button() self.location_page.click_create_button()
@ -91,6 +104,8 @@ class TestRackTab:
# Нажимаем кнопку создания # Нажимаем кнопку создания
create_child_frame.click_add_button() 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: def _delete_rack_from_context_menu(self, browser: Page, rack_name: str) -> None:
"""Удаляет стойку через контекстное меню. """Удаляет стойку через контекстное меню.
@ -118,9 +133,13 @@ class TestRackTab:
# Кликаем на кнопку "Изменить" # Кликаем на кнопку "Изменить"
rack_page.toolbar.get_button_by_name("edit").click() rack_page.toolbar.get_button_by_name("edit").click()
# 3. Используем метод click_remove_button, который обрабатывает весь процесс удаления # 3. Создаем экземпляр ModalRackEditRack
# включая диалог подтверждения rack_edit = ModalEditRack(browser, rack_name)
rack_page.click_remove_button()
# Используем метод для удаления
rack_edit.click_remove_button()
logger.info(f"Rack '{rack_name}' deleted successfully")
@pytest.fixture(scope="function", autouse=True) @pytest.fixture(scope="function", autouse=True)
def setup(self, browser: Page) -> None: def setup(self, browser: Page) -> None:
@ -134,6 +153,7 @@ class TestRackTab:
Args: Args:
browser (Page): Экземпляр страницы Playwright для взаимодействия с UI browser (Page): Экземпляр страницы Playwright для взаимодействия с UI
""" """
# Авторизация в системе # Авторизация в системе
login_page = LoginPage(browser) login_page = LoginPage(browser)
login_page.do_login() login_page.do_login()
@ -154,11 +174,15 @@ class TestRackTab:
if not self._check_rack_existance(browser, RACK_NAME): if not self._check_rack_existance(browser, RACK_NAME):
self._create_rack(browser, RACK_NAME) self._create_rack(browser, RACK_NAME)
self.main_page.wait_for_timeout(3000) 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.click_subpanel_item(RACK_NAME, parent="test-zone")
self.main_page.wait_for_timeout(3000) self.main_page.wait_for_timeout(3000)
logger.info("Test setup completed")
@pytest.fixture(scope="class", autouse=True) @pytest.fixture(scope="class", autouse=True)
def cleanup_rack(self, browser: Page): def cleanup_rack(self, browser: Page):
"""Фикстура для очистки созданной стойки после ВСЕХ тестов класса. """Фикстура для очистки созданной стойки после ВСЕХ тестов класса.
@ -169,6 +193,7 @@ class TestRackTab:
Args: Args:
browser: Экземпляр страницы Playwright browser: Экземпляр страницы Playwright
""" """
# Тесты выполняются здесь # Тесты выполняются здесь
yield yield
@ -199,6 +224,7 @@ class TestRackTab:
self.main_page.click_subpanel_item("test-zone") self.main_page.click_subpanel_item("test-zone")
self.main_page.wait_for_timeout(1000) self.main_page.wait_for_timeout(1000)
#@pytest.mark.develop #@pytest.mark.develop
def test_rack_tab_content(self, browser: Page) -> None: def test_rack_tab_content(self, browser: Page) -> None:
"""Тест содержимого вкладки 'Стойка'. """Тест содержимого вкладки 'Стойка'.
@ -233,6 +259,9 @@ class TestRackTab:
rt.wait_for_timeout(1000) 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: def test_rack_tab_switching(self, browser: Page) -> None:
"""Тест переключения между вкладками стойки оборудования. """Тест переключения между вкладками стойки оборудования.
@ -251,3 +280,240 @@ class TestRackTab:
# Переход в режим редактирования # Переход в режим редактирования
rt.should_be_toolbar_buttons() rt.should_be_toolbar_buttons()
rt.wait_for_timeout(1000) rt.wait_for_timeout(1000)
#@pytest.mark.develop
def test_rack_general_info_tab_fields(self, browser: Page) -> None:
"""Тест заполнения полей вкладки 'Общая информация' стойки."""
rt = RackPage(browser)
# Переходим в режим редактирования
logger.debug("Switching to edit mode...")
rt.click_edit_button()
rt.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,
)
# Заполняем все поля формы
logger.info("Filling rack form using fill_rack_data method...")
results = rack_edit.fill_rack_data(rack_edit_data)
logger.info(f"Fill results: {results}")
# Сохраняем изменения
logger.info("Saving changes...")
rack_edit.click_done_button()
rack_edit.wait_for_timeout(3000)
# Вход в режим редактирования
rt.click_edit_button()
# Проверяем поля, пропуская недоступные
verification_results = rack_edit.verify_all_filled_fields(
rack_edit_data,
skip_fields=["Владелец", "Обслуживающая организация", "Проект/Титул"]
)
logger.info(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:
"""Тест вкладки 'Изображение' стойки."""
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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -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'"

View File

@ -10,9 +10,7 @@ from pages.login_page import LoginPage
from pages.main_page import MainPage from pages.main_page import MainPage
from pages.ztp_templates_tab import ZTPTemplatesTab from pages.ztp_templates_tab import ZTPTemplatesTab
pytest.skip("Пропуск всех тестов в этом файле в связи исключением данной функциональности", allow_module_level=True)
# @pytest.mark.smoke
class TestZTPTemplatesTab: class TestZTPTemplatesTab:
"""Набор тестов для вкладки 'Шаблоны' в модуле Zero Touch Provisioning. """Набор тестов для вкладки 'Шаблоны' в модуле Zero Touch Provisioning.
@ -23,10 +21,9 @@ class TestZTPTemplatesTab:
1. test_templates_tab_content - Базовая структура и содержимое вкладки 1. test_templates_tab_content - Базовая структура и содержимое вкладки
2. test_templates_table_row_highlighting - Визуальное выделение строк таблицы 2. test_templates_table_row_highlighting - Визуальное выделение строк таблицы
3. test_templates_table_scrolling - Навигация по таблице с большим объемом данных 3. test_templates_table_scrolling - Навигация по таблице с большим объемом данных
4. test_templates_modal_window_close_buttons - Закрытие модальных окон разными способами 4. test_templates_modal_window_content - Структура и содержимое модальных окон
5. test_templates_modal_window_content - Структура и содержимое модальных окон 5. test_templates_modal_window_scrolling - Навигация в модальных окнах
6. test_templates_modal_window_scrolling - Навигация в модальных окнах 6. test_templates_modal_window_api_data_consistency - Синхронизация данных UI и API
7. test_templates_modal_window_api_data_consistency - Синхронизация данных UI и API
Фикстура setup обеспечивает подготовку тестового окружения: Фикстура setup обеспечивает подготовку тестового окружения:
- Авторизацию в системе - Авторизацию в системе
@ -56,7 +53,7 @@ class TestZTPTemplatesTab:
main_page.click_subpanel_item("Шаблоны", parent="Zero Touch Provisioning") main_page.click_subpanel_item("Шаблоны", parent="Zero Touch Provisioning")
main_page.wait_for_timeout(5000) main_page.wait_for_timeout(5000)
# @pytest.mark.develop
def test_templates_tab_content(self, browser: Page) -> None: def test_templates_tab_content(self, browser: Page) -> None:
"""Тест содержимого вкладки 'Шаблоны'. """Тест содержимого вкладки 'Шаблоны'.
@ -77,7 +74,7 @@ class TestZTPTemplatesTab:
browser.wait_for_timeout(5000) 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: 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() 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: def test_templates_modal_window_content(self, browser: Page) -> None:
"""Тест содержимого модального окна шаблона. """Тест содержимого модального окна шаблона.
@ -215,7 +161,7 @@ class TestZTPTemplatesTab:
browser.wait_for_timeout(2000) 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) browser.wait_for_timeout(1000)
@ -224,14 +170,15 @@ class TestZTPTemplatesTab:
ztp_templates_tab.should_be_modal_window() 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() ztp_templates_tab.should_not_be_modal_window()
#@pytest.mark.skip(reason=" Временно исключено из тестирования")
def test_templates_modal_window_scrolling(self, browser: Page) -> None: def test_templates_modal_window_scrolling(self, browser: Page) -> None:
"""Тест скроллинга модального окна шаблона. """Тест скроллинга модального окна шаблона.
@ -251,7 +198,7 @@ class TestZTPTemplatesTab:
browser.wait_for_timeout(2000) 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) browser.wait_for_timeout(1000)
@ -265,22 +212,23 @@ class TestZTPTemplatesTab:
if is_scrollable: if is_scrollable:
print("Модальное окно поддерживает вертикальный скроллинг") print("Модальное окно поддерживает вертикальный скроллинг")
# Прокрутка вниз модального окна # Прокрутка вниз
ztp_templates_tab.scroll_modal_down() ztp_templates_tab.scroll_modal_down()
browser.wait_for_timeout(1000) browser.wait_for_timeout(1000)
# Прокрутка вверх модального окна # Прокрутка вверх
ztp_templates_tab.scroll_modal_up() ztp_templates_tab.scroll_modal_up()
browser.wait_for_timeout(1000) browser.wait_for_timeout(1000)
else: else:
print("Модальное окно не поддерживает вертикальный скроллинг") 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() ztp_templates_tab.should_not_be_modal_window()
#@pytest.mark.skip(reason=" Временно исключено из тестирования")
def test_templates_modal_window_api_data_consistency(self, browser: Page) -> None: def test_templates_modal_window_api_data_consistency(self, browser: Page) -> None:
"""Тест соответствия данных модального окна шаблона данным из API. """Тест соответствия данных модального окна шаблона данным из API.
@ -304,7 +252,7 @@ class TestZTPTemplatesTab:
browser.wait_for_timeout(5000) 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) browser.wait_for_timeout(2000)
@ -313,10 +261,10 @@ class TestZTPTemplatesTab:
ztp_templates_tab.should_be_modal_window() ztp_templates_tab.should_be_modal_window()
# Проверка соответствия данных модального окна данным из API # Проверка соответствия данных модального окна данным из 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() ztp_templates_tab.should_not_be_modal_window()