e-nms_qa_automation/components_derived/modal_edit_rack.py

1402 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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