1142 lines
42 KiB
Python
1142 lines
42 KiB
Python
"""Модуль для работы с модальным окном редактирования стойки."""
|
||
|
||
import re
|
||
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 components.modal_window_component import ModalWindowComponent
|
||
from components.dropdown_list_component import DropdownList
|
||
from components.confirm_component import ConfirmComponent
|
||
from elements.text_input_element import TextInput
|
||
from elements.text_element import Text
|
||
from forms.edit_rack_form import EditRackForm, EditRackFormData
|
||
|
||
|
||
logger = get_logger("EDIT_RACK_MAKER")
|
||
logger.setLevel("INFO")
|
||
|
||
|
||
# Используем EditRackFormData
|
||
EditRackData = EditRackFormData
|
||
|
||
|
||
class EditRackMaker(ModalWindowComponent):
|
||
"""Компонент для работы с модальным окном редактирования стойки.
|
||
|
||
Предоставляет методы для взаимодействия с элементами окна:
|
||
- переключение между вкладками
|
||
- заполнение полей общей информации (через EditRackForm)
|
||
- работа с изображениями
|
||
- настройка правил доступа
|
||
- сохранение/отмена изменений
|
||
- удаление стойки
|
||
"""
|
||
|
||
# Константы для названий вкладок
|
||
TAB_GENERAL = "Общая информация"
|
||
TAB_IMAGE = "Изображение"
|
||
TAB_SETTINGS = "Настройки"
|
||
|
||
# Маппинг полей для вкладки "Настройки" - оставляем только то, что специфично для модального окна
|
||
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.active_tab = self.TAB_GENERAL
|
||
self.tabs = {}
|
||
self.content_items = {}
|
||
self.delete_confirm = None
|
||
self.edit_form = 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.edit_form = EditRackForm(self.page)
|
||
# Копируем content_items из формы
|
||
self.content_items.update(self.edit_form.content_items)
|
||
logger.debug("General tab content initialized via EditRackForm")
|
||
|
||
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 [
|
||
"admin",
|
||
"manager",
|
||
"operator",
|
||
"sec",
|
||
"collector"
|
||
]
|
||
|
||
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: EditRackData) -> dict:
|
||
"""Заполняет поля формы редактирования стойки.
|
||
|
||
Args:
|
||
rack_data: Данные для заполнения.
|
||
|
||
Returns:
|
||
Словарь с результатами заполнения.
|
||
"""
|
||
|
||
if self.active_tab != self.TAB_GENERAL:
|
||
self.switch_to_tab(self.TAB_GENERAL)
|
||
|
||
# Используем форму для заполнения данных
|
||
if self.edit_form:
|
||
results = self.edit_form.fill_rack_data(rack_data)
|
||
logger.info(f"Filled rack data via EditRackForm: {results}")
|
||
return results
|
||
else:
|
||
logger.error("Edit form not initialized")
|
||
return {
|
||
"text_fields_filled": 0,
|
||
"combobox_fields_filled": 0,
|
||
"checkboxes_set": 0
|
||
}
|
||
|
||
# Проверки
|
||
def verify_all_filled_fields(
|
||
self,
|
||
rack_data: EditRackData,
|
||
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)
|
||
|
||
# Проверяем combobox поля
|
||
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: EditRackData,
|
||
skip_fields: List[str],
|
||
results: dict
|
||
) -> None:
|
||
"""Проверяет текстовые поля.
|
||
|
||
Args:
|
||
rack_data: Данные для проверки.
|
||
skip_fields: Список полей для пропуска.
|
||
results: Словарь с результатами для обновления.
|
||
"""
|
||
|
||
if not self.edit_form:
|
||
logger.error("Edit form not initialized")
|
||
return
|
||
|
||
for field_label, (attr_name, field_name) in self.edit_form.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.edit_form.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: EditRackData,
|
||
skip_fields: List[str],
|
||
results: dict
|
||
) -> None:
|
||
"""Проверяет combobox поля.
|
||
|
||
Args:
|
||
rack_data: Данные для проверки.
|
||
skip_fields: Список полей для пропуска.
|
||
results: Словарь с результатами для обновления.
|
||
"""
|
||
|
||
if not self.edit_form:
|
||
logger.error("Edit form not initialized")
|
||
return
|
||
|
||
for field_label, (attr_name, _, _) in self.edit_form.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:
|
||
Значение поля или пустая строка.
|
||
"""
|
||
|
||
if not self.edit_form:
|
||
return ""
|
||
|
||
actual_value = ""
|
||
# Используем локаторы из edit_form
|
||
locator = self.edit_form.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: EditRackData,
|
||
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.edit_form.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)}")
|