рефакторинг
parent
c56bf70cfa
commit
852b42f226
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
# ...
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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:
|
||||||
"""Прокручивает содержимое окна вниз."""
|
"""Прокручивает содержимое окна вниз."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -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']"
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ class RackLocators:
|
||||||
|
|
||||||
# Чекбоксы
|
# Чекбоксы
|
||||||
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel'] input[type='checkbox']"
|
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION = "[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
|
# Локаторы для меню combobox
|
||||||
MENU_ACTIVE_RACK_FORM = "//div[contains(@class, 'menuable__content__active')]"
|
MENU_ACTIVE_RACK_FORM = "//div[contains(@class, 'menuable__content__active')]"
|
||||||
|
|
@ -122,3 +124,34 @@ class RackLocators:
|
||||||
# Кнопки подтверждения удаления
|
# Кнопки подтверждения удаления
|
||||||
CONFIRM_REMOVE_YES_BUTTON = "[data-testid='cabinet-bar__card_confirmation__btn__yes']"
|
CONFIRM_REMOVE_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']"
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import os
|
||||||
from playwright.sync_api import Page, expect
|
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 pages.location_page import LocationPage
|
||||||
|
|
@ -19,6 +20,10 @@ 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:
|
||||||
"""Набор тестов для вкладки 'Стойка' в модуле Объекты.
|
"""Набор тестов для вкладки 'Стойка' в модуле Объекты.
|
||||||
|
|
||||||
|
|
@ -28,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 - Навигация по всем вкладкам модального окна редактирования
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Инициализируем атрибуты
|
# Инициализируем атрибуты
|
||||||
|
|
@ -65,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()
|
||||||
|
|
||||||
|
|
@ -93,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:
|
||||||
"""Удаляет стойку через контекстное меню.
|
"""Удаляет стойку через контекстное меню.
|
||||||
|
|
||||||
|
|
@ -126,22 +139,7 @@ class TestRackTab:
|
||||||
# Используем метод для удаления
|
# Используем метод для удаления
|
||||||
rack_edit.click_remove_button()
|
rack_edit.click_remove_button()
|
||||||
|
|
||||||
# 4. Проверяем уведомление об успешном удалении - требуется создать разработчику (заведена задача)
|
logger.info(f"Rack '{rack_name}' deleted successfully")
|
||||||
# Создаем экземпляр фрейма для доступа к alert компоненту
|
|
||||||
# create_child_frame = CreateChildElementFrame(browser)
|
|
||||||
|
|
||||||
# Проверяем наличие любого alert-окна (не обязательно точного текста)
|
|
||||||
# create_child_frame.alert.check_alert_presence("")
|
|
||||||
|
|
||||||
# Получаем текст alert, чтобы убедиться что удаление прошло успешно
|
|
||||||
# alert_text = create_child_frame.alert.get_text()
|
|
||||||
# logger.debug(f"Alert text after deletion: {alert_text}")
|
|
||||||
|
|
||||||
# Проверяем что в тексте есть указание на успешное удаление
|
|
||||||
# assert "удален" in alert_text.lower() or "успешно" in alert_text.lower()
|
|
||||||
|
|
||||||
# Закрываем alert
|
|
||||||
# create_child_frame.alert.close_alert()
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function", autouse=True)
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
def setup(self, browser: Page) -> None:
|
def setup(self, browser: Page) -> None:
|
||||||
|
|
@ -155,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()
|
||||||
|
|
@ -175,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):
|
||||||
"""Фикстура для очистки созданной стойки после ВСЕХ тестов класса.
|
"""Фикстура для очистки созданной стойки после ВСЕХ тестов класса.
|
||||||
|
|
@ -190,6 +193,7 @@ class TestRackTab:
|
||||||
Args:
|
Args:
|
||||||
browser: Экземпляр страницы Playwright
|
browser: Экземпляр страницы Playwright
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Тесты выполняются здесь
|
# Тесты выполняются здесь
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
@ -220,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:
|
||||||
"""Тест содержимого вкладки 'Стойка'.
|
"""Тест содержимого вкладки 'Стойка'.
|
||||||
|
|
@ -254,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:
|
||||||
"""Тест переключения между вкладками стойки оборудования.
|
"""Тест переключения между вкладками стойки оборудования.
|
||||||
|
|
||||||
|
|
@ -273,10 +281,10 @@ 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
|
|
||||||
|
#@pytest.mark.develop
|
||||||
def test_rack_general_info_tab_fields(self, browser: Page) -> None:
|
def test_rack_general_info_tab_fields(self, browser: Page) -> None:
|
||||||
"""Тест заполнения полей вкладки 'Общая информация' стойки."""
|
"""Тест заполнения полей вкладки 'Общая информация' стойки."""
|
||||||
logger = get_logger("RACK_GENERAL_INFO_TEST")
|
|
||||||
|
|
||||||
rt = RackPage(browser)
|
rt = RackPage(browser)
|
||||||
|
|
||||||
|
|
@ -286,7 +294,7 @@ class TestRackTab:
|
||||||
rt.wait_for_timeout(1000)
|
rt.wait_for_timeout(1000)
|
||||||
|
|
||||||
# Создаем экземпляр ModalEditRack
|
# Создаем экземпляр ModalEditRack
|
||||||
rack_edit = ModalEditRack(browser, RACK_NAME) # ИЗМЕНЕНО: добавлен RACK_NAME
|
rack_edit = ModalEditRack(browser, RACK_NAME)
|
||||||
|
|
||||||
# Создаем тестовые данные для заполнения всех полей
|
# Создаем тестовые данные для заполнения всех полей
|
||||||
rack_edit_data = RackEditData(
|
rack_edit_data = RackEditData(
|
||||||
|
|
@ -306,13 +314,6 @@ class TestRackTab:
|
||||||
|
|
||||||
# Checkbox поля
|
# Checkbox поля
|
||||||
ventilation_panel=True,
|
ventilation_panel=True,
|
||||||
|
|
||||||
# Правила доступа (если есть такие поля в форме)
|
|
||||||
#read_access_rules="admin" if "Правила доступа для чтения по умолчанию" in available_fields else "",
|
|
||||||
#write_access_rules="admin" if "Правила доступа для записи по умолчанию" in available_fields else "",
|
|
||||||
#sms_access_rules="admin" if "Правила доступа по умолчанию для получения СМС" in available_fields else "",
|
|
||||||
#email_access_rules="admin" if "Правила доступа по умолчанию для получения email сообщения" in available_fields else "",
|
|
||||||
#push_access_rules="admin" if "Правила доступа по умолчанию для получения push уведомлений" in available_fields else "",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Заполняем все поля формы
|
# Заполняем все поля формы
|
||||||
|
|
@ -326,9 +327,6 @@ class TestRackTab:
|
||||||
rack_edit.click_done_button()
|
rack_edit.click_done_button()
|
||||||
rack_edit.wait_for_timeout(3000)
|
rack_edit.wait_for_timeout(3000)
|
||||||
|
|
||||||
# Проверяем поля, которые мы заполнили, действительно заполнены
|
|
||||||
logger.info("=== Проверка, что все поля корректно заполнены ===")
|
|
||||||
|
|
||||||
# Вход в режим редактирования
|
# Вход в режим редактирования
|
||||||
rt.click_edit_button()
|
rt.click_edit_button()
|
||||||
|
|
||||||
|
|
@ -341,10 +339,181 @@ class TestRackTab:
|
||||||
|
|
||||||
# Проверяем результаты
|
# Проверяем результаты
|
||||||
assert verification_results["incorrectly_filled"] == 0, \
|
assert verification_results["incorrectly_filled"] == 0, \
|
||||||
f"Некорректно заполнены доступные поля: {verification_results['field_errors']}"
|
f"Available fields incorrectly filled: {verification_results['field_errors']}"
|
||||||
assert verification_results["not_filled"] == 0, \
|
assert verification_results["not_filled"] == 0, \
|
||||||
f"Не заполнены доступные поля: {verification_results['field_errors']}"
|
f"Available fields not filled: {verification_results['field_errors']}"
|
||||||
|
|
||||||
rack_edit.click_close_button()
|
rack_edit.click_close_button()
|
||||||
|
|
||||||
logger.info("✓ General Info tab fields test completed")
|
|
||||||
|
#@pytest.mark.develop
|
||||||
|
def test_rack_image_tab(self, browser: Page) -> None:
|
||||||
|
"""Тест вкладки 'Изображение' стойки."""
|
||||||
|
|
||||||
|
rt = RackPage(browser)
|
||||||
|
|
||||||
|
# Переходим в режим редактирования
|
||||||
|
rt.click_edit_button()
|
||||||
|
rt.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
# Создаем экземпляр ModalEditRack
|
||||||
|
rack_edit = ModalEditRack(browser, RACK_NAME)
|
||||||
|
|
||||||
|
# Переключаемся на вкладку "Изображение"
|
||||||
|
rack_edit.switch_to_tab(ModalEditRack.TAB_IMAGE)
|
||||||
|
|
||||||
|
# Проверяем вкладку
|
||||||
|
assert rack_edit.is_tab_active(ModalEditRack.TAB_IMAGE), "Image tab should be active"
|
||||||
|
|
||||||
|
# Загружаем изображение если есть
|
||||||
|
test_image_path = os.path.join(os.path.dirname(__file__), "test_image.jpg")
|
||||||
|
if os.path.exists(test_image_path):
|
||||||
|
logger.info(f"Found test image: {test_image_path}")
|
||||||
|
# Находим input и загружаем файл
|
||||||
|
file_input = browser.locator("input[type='file']")
|
||||||
|
if file_input.count() == 0:
|
||||||
|
file_input = browser.locator(".button-file-upload__input")
|
||||||
|
|
||||||
|
if file_input.count() > 0:
|
||||||
|
file_input.set_input_files(test_image_path)
|
||||||
|
rt.wait_for_timeout(2000)
|
||||||
|
logger.info("Test image uploaded")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Test image not found at: {test_image_path}")
|
||||||
|
|
||||||
|
# Сохраняем
|
||||||
|
rack_edit.click_done_button()
|
||||||
|
|
||||||
|
#@pytest.mark.develop
|
||||||
|
def test_rack_access_rules(self, browser: Page) -> None:
|
||||||
|
"""Тест заполнения полей правил доступа.
|
||||||
|
|
||||||
|
В каждое поле добавляются ВСЕ пользователи из списка custom_users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rt = RackPage(browser)
|
||||||
|
|
||||||
|
# Переходим в режим редактирования
|
||||||
|
rt.click_edit_button()
|
||||||
|
rt.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
# Создаем экземпляр ModalEditRack
|
||||||
|
rack_edit = ModalEditRack(browser, RACK_NAME)
|
||||||
|
|
||||||
|
# Переключаемся на вкладку "Настройки"
|
||||||
|
rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS)
|
||||||
|
|
||||||
|
# Проверяем, что вкладка активна
|
||||||
|
assert rack_edit.is_tab_active(ModalEditRack.TAB_SETTINGS), \
|
||||||
|
"Settings tab should be active after switching"
|
||||||
|
|
||||||
|
# Целевые поля для заполнения
|
||||||
|
target_fields = [
|
||||||
|
"read_access_rules",
|
||||||
|
"write_access_rules",
|
||||||
|
"sms_access_rules",
|
||||||
|
"email_access_rules",
|
||||||
|
"push_access_rules"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Пользователи для добавления в каждое поле
|
||||||
|
custom_users = [
|
||||||
|
"TestUserRulesAdmin",
|
||||||
|
"TestUserRulesOper",
|
||||||
|
"TestUserRulesManager",
|
||||||
|
"TestUserRulesSec",
|
||||||
|
"TestUserRulesCollector"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Заполняем поля - в КАЖДОЕ поле добавляются ВСЕ пользователи из custom_users
|
||||||
|
fill_results = rack_edit.fill_access_rules(
|
||||||
|
users_to_add=custom_users,
|
||||||
|
target_fields=target_fields
|
||||||
|
)
|
||||||
|
# Проверяем, что все пользователи были добавлены
|
||||||
|
expected_total = len(target_fields) * len(custom_users)
|
||||||
|
assert fill_results["access_rules_filled"] == expected_total, \
|
||||||
|
f"Added {fill_results['access_rules_filled']} users, expected {expected_total}"
|
||||||
|
assert len(fill_results["errors"]) == 0, \
|
||||||
|
f"Errors during filling: {fill_results['errors']}"
|
||||||
|
|
||||||
|
# Проверяем заполнение - в КАЖДОМ поле должны быть ВСЕ пользователи
|
||||||
|
verification_results = rack_edit.verify_access_rules(
|
||||||
|
expected_users=custom_users,
|
||||||
|
target_fields=target_fields
|
||||||
|
)
|
||||||
|
logger.info(f"Verification results: {verification_results}")
|
||||||
|
|
||||||
|
# Проверяем результаты
|
||||||
|
assert verification_results["correctly_filled"] == expected_total, \
|
||||||
|
f"Correctly filled {verification_results['correctly_filled']} out of {expected_total}"
|
||||||
|
assert verification_results["incorrectly_filled"] == 0, \
|
||||||
|
f"Verification errors: {verification_results['field_errors']}"
|
||||||
|
|
||||||
|
# Дополнительная проверка - убеждаемся, что все поля были проверены
|
||||||
|
assert len(verification_results["fields_verified"]) == len(target_fields), \
|
||||||
|
f"Fields verified: {len(verification_results['fields_verified'])}, expected: {len(target_fields)}"
|
||||||
|
|
||||||
|
# Сохраняем изменения
|
||||||
|
rack_edit.click_done_button()
|
||||||
|
rack_edit.wait_for_timeout(3000)
|
||||||
|
|
||||||
|
# Возвращаемся в режим редактирования и проверяем снова
|
||||||
|
rt.click_edit_button()
|
||||||
|
rt.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
rack_edit = ModalEditRack(browser, RACK_NAME)
|
||||||
|
rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS)
|
||||||
|
|
||||||
|
verification_results_after_save = rack_edit.verify_access_rules(
|
||||||
|
expected_users=custom_users,
|
||||||
|
target_fields=target_fields
|
||||||
|
)
|
||||||
|
logger.debug(f"Verification results after save: {verification_results_after_save}")
|
||||||
|
|
||||||
|
assert verification_results_after_save["correctly_filled"] == expected_total, \
|
||||||
|
f"After save - correctly filled {verification_results_after_save['correctly_filled']} out of {expected_total}"
|
||||||
|
|
||||||
|
rack_edit.click_close_button()
|
||||||
|
|
||||||
|
|
||||||
|
#@pytest.mark.develop
|
||||||
|
def test_rack_all_tabs_navigation(self, browser: Page) -> None:
|
||||||
|
"""Тест навигации по всем вкладкам модального окна редактирования стойки."""
|
||||||
|
|
||||||
|
rt = RackPage(browser)
|
||||||
|
|
||||||
|
# Переходим в режим редактирования
|
||||||
|
rt.click_edit_button()
|
||||||
|
rt.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
# Создаем экземпляр ModalEditRack
|
||||||
|
rack_edit = ModalEditRack(browser, RACK_NAME)
|
||||||
|
|
||||||
|
# Проверяем начальное состояние
|
||||||
|
initial_tab = rack_edit.get_active_tab()
|
||||||
|
assert initial_tab == ModalEditRack.TAB_GENERAL, \
|
||||||
|
f"Initial tab should be '{ModalEditRack.TAB_GENERAL}', got '{initial_tab}'"
|
||||||
|
logger.debug(f"Initial tab: {initial_tab}")
|
||||||
|
|
||||||
|
# Переключаемся на вкладку "Изображение"
|
||||||
|
rack_edit.switch_to_tab(ModalEditRack.TAB_IMAGE)
|
||||||
|
assert rack_edit.is_tab_active(ModalEditRack.TAB_IMAGE), \
|
||||||
|
"Should be on Image tab after switching"
|
||||||
|
logger.debug(f"Switched to: {ModalEditRack.TAB_IMAGE}")
|
||||||
|
|
||||||
|
# Переключаемся на вкладку "Настройки"
|
||||||
|
rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS)
|
||||||
|
assert rack_edit.is_tab_active(ModalEditRack.TAB_SETTINGS), \
|
||||||
|
"Should be on Settings tab after switching"
|
||||||
|
logger.debug(f"Switched to: {ModalEditRack.TAB_SETTINGS}")
|
||||||
|
|
||||||
|
# Возвращаемся на вкладку "Общая информация"
|
||||||
|
rack_edit.switch_to_tab(ModalEditRack.TAB_GENERAL)
|
||||||
|
assert rack_edit.is_tab_active(ModalEditRack.TAB_GENERAL), \
|
||||||
|
"Should be back on General tab after switching"
|
||||||
|
logger.debug(f"Switched back to: {ModalEditRack.TAB_GENERAL}")
|
||||||
|
|
||||||
|
# Закрываем окно редактирования
|
||||||
|
rack_edit.click_close_button()
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
|
|
@ -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'"
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue