465 lines
19 KiB
Python
465 lines
19 KiB
Python
"""Модуль для работы с модальным окном редактирования стойки."""
|
||
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional, Dict
|
||
from playwright.sync_api import Page, Locator
|
||
from tools.logger import get_logger
|
||
from locators.rack_locators import RackLocators
|
||
from elements.tooltip_button_element import TooltipButton
|
||
from components.toolbar_component import ToolbarComponent
|
||
from components.base_component import BaseComponent
|
||
|
||
logger = get_logger("MODAL_RACK_EDIT")
|
||
#logger.setLevel("INFO")
|
||
|
||
|
||
@dataclass
|
||
class RackEditData:
|
||
"""Класс для хранения данных редактирования стойки."""
|
||
|
||
# Основные поля (редактируемые)
|
||
name: str = ""
|
||
serial: str = ""
|
||
inventory: str = ""
|
||
comment: str = ""
|
||
|
||
# Combobox поля (редактируемые)
|
||
cable_entry: str = ""
|
||
state: str = ""
|
||
owner: str = ""
|
||
service_org: str = ""
|
||
project: str = ""
|
||
|
||
# Дополнительные поля (редактируемые)
|
||
power: 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 ModalRackEdit(BaseComponent):
|
||
"""Компонент для работы с модальным окном редактирования стойки."""
|
||
|
||
def __init__(self, page: Page) -> None:
|
||
"""
|
||
Инициализирует компонент редактирования стойки.
|
||
|
||
Args:
|
||
page (Page): Экземпляр страницы Playwright
|
||
"""
|
||
super().__init__(page)
|
||
self._form_container = None
|
||
self._fields_cache = {}
|
||
self.toolbar = ToolbarComponent(page, "")
|
||
|
||
# Кнопка "Переместить" (data-testid)
|
||
replace_button_locator = self.page.locator(RackLocators.TOOLBAR_REPLACE_BUTTON)
|
||
self.replace_button = TooltipButton(page, replace_button_locator, "replace")
|
||
|
||
# Кнопка "Сохранить" (data-testid)
|
||
done_button_locator = page.locator(RackLocators.TOOLBAR_DONE_BUTTON)
|
||
self.done_button = TooltipButton(page, done_button_locator, "done")
|
||
|
||
# Кнопка "Отменить" (data-testid)
|
||
close_button_locator = page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON)
|
||
self.close_button = TooltipButton(page, close_button_locator, "close")
|
||
|
||
# Кнопка "Удалить" (data-testid)
|
||
remove_button_locator = page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON)
|
||
self.remove_button = TooltipButton(page, remove_button_locator, "remove")
|
||
|
||
# Добавляем кнопки в тулбар
|
||
self.toolbar.add_tooltip_button(replace_button_locator, "replace")
|
||
self.toolbar.add_tooltip_button(done_button_locator, "done")
|
||
self.toolbar.add_tooltip_button(close_button_locator, "close")
|
||
self.toolbar.add_tooltip_button(remove_button_locator, "remove")
|
||
|
||
def click_remove_button(self) -> None:
|
||
"""
|
||
Кликает на кнопку 'Удалить' и обрабатывает диалог подтверждения.
|
||
"""
|
||
logger.debug("Clicking on 'Remove' button...")
|
||
|
||
# Проверяем видимость кнопки
|
||
self.toolbar.check_button_visibility("remove")
|
||
self.toolbar.check_button_tooltip("remove", "Удалить")
|
||
|
||
# Кликаем на кнопку удаления
|
||
self.toolbar.get_button_by_name("remove").click()
|
||
self.wait_for_timeout(1000)
|
||
|
||
# Ожидаем появления диалога подтверждения
|
||
self._handle_remove_confirmation_dialog()
|
||
|
||
def click_done_button(self) -> None:
|
||
"""
|
||
Кликает на кнопку 'Сохранить' и обрабатывает диалог подтверждения.
|
||
"""
|
||
logger.debug("Clicking on 'Done' button...")
|
||
|
||
# Проверяем видимость кнопки
|
||
self.toolbar.check_button_visibility("done")
|
||
self.toolbar.check_button_tooltip("done", "Сохранить")
|
||
|
||
# Кликаем на кнопку удаления
|
||
self.toolbar.get_button_by_name("done").click()
|
||
self.wait_for_timeout(1000)
|
||
|
||
def confirm_remove_dialog(self, confirm: bool = True) -> None:
|
||
"""
|
||
Подтверждает или отклоняет удаление в диалоговом окне.
|
||
|
||
Args:
|
||
confirm (bool): Если True - подтвердить удаление, если False - отменить
|
||
"""
|
||
logger.debug(f"Confirming remove dialog with: {'Да' if confirm else 'Нет'}")
|
||
|
||
# Ждем немного перед поиском диалога
|
||
self.wait_for_timeout(1500)
|
||
|
||
# Ищем активный диалог
|
||
dialog = self.page.locator("div.v-dialog--active")
|
||
|
||
# Проверяем, что диалог найден и содержит нужный текст
|
||
assert dialog.count() > 0, "No active dialog found"
|
||
|
||
# Проверяем текст диалога
|
||
dialog_text = dialog.first.text_content()
|
||
logger.debug("Dialog text: %s", dialog_text)
|
||
|
||
# Должен содержать "Запрос подтверждения" и "Удалить"
|
||
assert "Запрос подтверждения" in dialog_text, "Not a confirmation dialog"
|
||
|
||
# Ищем кнопку по data-testid
|
||
if confirm:
|
||
button = self.page.locator(RackLocators.CONFIRM_REMOVE_YES_BUTTON)
|
||
else:
|
||
button = self.page.locator(RackLocators.CONFIRM_REMOVE_NO_BUTTON)
|
||
|
||
# Проверяем, что кнопка найдена
|
||
assert button.count() > 0, "Button not found with selector"
|
||
|
||
# Кликаем на кнопку
|
||
button_text = button.first.text_content()
|
||
logger.debug("Clicking button with text: %s", button_text)
|
||
button.first.click()
|
||
self.wait_for_timeout(2000)
|
||
|
||
# Проверяем, что диалог закрылся
|
||
self.wait_for_timeout(1000)
|
||
|
||
logger.debug("Remove confirmation completed")
|
||
|
||
def _handle_remove_confirmation_dialog(self) -> None:
|
||
"""Обрабатывает диалог подтверждения удаления."""
|
||
logger.debug("Handling remove confirmation dialog...")
|
||
self.confirm_remove_dialog(confirm=True)
|
||
|
||
# Остальные существующие методы остаются без изменений
|
||
def _get_form_container(self) -> Locator:
|
||
"""
|
||
Получает контейнер формы редактирования.
|
||
"""
|
||
if self._form_container is None:
|
||
form_container = self.page.locator("[data-testid='cabinet-bar__cabinet-form']")
|
||
try:
|
||
form_container.wait_for(state="visible", timeout=10000)
|
||
self._form_container = form_container
|
||
except:
|
||
raise ValueError("Cabinet form container not found")
|
||
|
||
return self._form_container
|
||
|
||
def get_available_fields(self) -> list:
|
||
"""
|
||
Получает список доступных полей.
|
||
"""
|
||
fields_locators = self._get_form_fields()
|
||
return list(fields_locators.keys()) if fields_locators else []
|
||
|
||
def _get_form_fields(self) -> dict:
|
||
"""
|
||
Получает все поля формы редактирования стойки.
|
||
"""
|
||
if self._fields_cache:
|
||
return self._fields_cache
|
||
|
||
form_container = self._get_form_container()
|
||
fields_locators = self.get_input_fields_locators(form_container)
|
||
self._fields_cache = fields_locators
|
||
|
||
return fields_locators
|
||
|
||
def _fill_text_field(self, field_name: str, value: str) -> bool:
|
||
"""
|
||
Заполняет текстовое поле по полному совпадению названия.
|
||
"""
|
||
fields_locators = self._get_form_fields()
|
||
|
||
# Ищем точное совпадение
|
||
if field_name not in fields_locators:
|
||
logger.debug(f"Text field '{field_name}' not found. Available fields: {list(fields_locators.keys())}")
|
||
return False
|
||
|
||
field_container = fields_locators[field_name]
|
||
|
||
try:
|
||
field_container.scroll_into_view_if_needed()
|
||
self.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.wait_for_timeout(200)
|
||
input_field.fill("")
|
||
self.wait_for_timeout(200)
|
||
input_field.fill(value)
|
||
self.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 поле по полному совпадению названия.
|
||
"""
|
||
fields_locators = self._get_form_fields()
|
||
|
||
# Ищем точное совпадение
|
||
if field_name not in fields_locators:
|
||
logger.debug(f"Combobox field '{field_name}' not found. Available fields: {list(fields_locators.keys())}")
|
||
return False
|
||
|
||
field_container = fields_locators[field_name]
|
||
|
||
try:
|
||
field_container.scroll_into_view_if_needed()
|
||
self.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.wait_for_timeout(1000)
|
||
else:
|
||
dropdown_button.click()
|
||
self.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.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
|
||
|
||
def _set_checkbox_field(self, checkbox_label: str, value: bool) -> bool:
|
||
"""
|
||
Устанавливает состояние checkbox используя input[type="checkbox"].
|
||
"""
|
||
try:
|
||
logger.debug(f"Setting checkbox '{checkbox_label}' to {value}")
|
||
|
||
# Ищем все checkbox элементы в форме
|
||
form_container = self._get_form_container()
|
||
checkboxes = form_container.locator("input[type='checkbox']")
|
||
|
||
if checkboxes.count() == 0:
|
||
logger.warning("No checkbox elements found in form")
|
||
return False
|
||
|
||
logger.debug(f"Found {checkboxes.count()} checkbox(es) in form")
|
||
|
||
# Если несколько чекбоксов, ищем нужный
|
||
target_checkbox = None
|
||
|
||
# Вариант 1: По data-testid
|
||
for i in range(checkboxes.count()):
|
||
checkbox = checkboxes.nth(i)
|
||
testid = checkbox.get_attribute("data-testid")
|
||
if testid == "cabinet-bar__main__checkbox__available_ventilation_panel":
|
||
target_checkbox = checkbox
|
||
logger.debug(f"Found checkbox by data-testid: {testid}")
|
||
break
|
||
|
||
# Вариант 2: По role="checkbox" и aria-checked
|
||
if target_checkbox is None:
|
||
for i in range(checkboxes.count()):
|
||
checkbox = checkboxes.nth(i)
|
||
role = checkbox.get_attribute("role")
|
||
if role == "checkbox":
|
||
target_checkbox = checkbox
|
||
logger.debug(f"Found checkbox by role='checkbox'")
|
||
break
|
||
|
||
# Вариант 3: Первый найденный checkbox
|
||
if target_checkbox is None:
|
||
target_checkbox = checkboxes.first
|
||
logger.debug("Using first found checkbox")
|
||
|
||
# Проверяем состояние
|
||
current_aria_checked = target_checkbox.get_attribute("aria-checked")
|
||
is_currently_checked = current_aria_checked == "true"
|
||
logger.debug(f"Checkbox current state: {is_currently_checked}")
|
||
|
||
# Если уже в нужном состоянии
|
||
if is_currently_checked == value:
|
||
logger.debug(f"Checkbox already in desired state ({value})")
|
||
return True
|
||
|
||
# Кликаем на чекбокс
|
||
target_checkbox.click(force=True)
|
||
self.wait_for_timeout(800)
|
||
|
||
# Проверяем результат
|
||
new_aria_checked = target_checkbox.get_attribute("aria-checked")
|
||
is_now_checked = new_aria_checked == "true"
|
||
|
||
if is_now_checked == value:
|
||
logger.info(f"✓ Checkbox '{checkbox_label}' set to {value}")
|
||
return True
|
||
else:
|
||
logger.warning(f"Checkbox state didn't change. Still: {is_now_checked}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error setting checkbox '{checkbox_label}': {e}")
|
||
return False
|
||
|
||
def fill_rack_data(self, rack_data: RackEditData) -> Dict[str, int]:
|
||
"""
|
||
Заполняет все доступные поля в форме редактирования.
|
||
"""
|
||
logger.debug("Filling rack edit form...")
|
||
|
||
results = {
|
||
"text_fields_filled": 0,
|
||
"combobox_fields_filled": 0,
|
||
"checkboxes_set": 0
|
||
}
|
||
|
||
# Получаем доступные поля
|
||
available_fields = self.get_available_fields()
|
||
logger.debug(f"Available fields in form: {available_fields}")
|
||
|
||
# 1. Заполняем текстовые поля (только если поле существует)
|
||
text_fields_mapping = {
|
||
"Имя": rack_data.name,
|
||
"Серийный номер": rack_data.serial,
|
||
"Инвентарный номер": rack_data.inventory,
|
||
"Комментарий": rack_data.comment,
|
||
"Выделенная мощность": rack_data.power,
|
||
"Правила доступа для чтения по умолчанию": rack_data.read_access_rules,
|
||
"Правила доступа для записи по умолчанию": rack_data.write_access_rules,
|
||
"Правила доступа по умолчанию для получения СМС": rack_data.sms_access_rules,
|
||
"Правила доступа по умолчанию для получения email сообщения": rack_data.email_access_rules,
|
||
"Правила доступа по умолчанию для получения push уведомлений": rack_data.push_access_rules
|
||
}
|
||
|
||
for field_name, value in text_fields_mapping.items():
|
||
if value and value.strip() and field_name in available_fields:
|
||
if self._fill_text_field(field_name, value):
|
||
results["text_fields_filled"] += 1
|
||
|
||
# 2. Заполняем combobox поля (только если поле существует)
|
||
combobox_fields_mapping = {
|
||
"Ввод кабеля": rack_data.cable_entry,
|
||
"Состояние": rack_data.state,
|
||
"Владелец": rack_data.owner,
|
||
"Обслуживающая организация": rack_data.service_org,
|
||
"Проект/Титул": rack_data.project
|
||
}
|
||
|
||
for field_name, value in combobox_fields_mapping.items():
|
||
if value and value.strip() and field_name in available_fields:
|
||
if self._fill_combobox_field(field_name, value):
|
||
results["combobox_fields_filled"] += 1
|
||
|
||
# 3. Устанавливаем checkbox (если есть)
|
||
if rack_data.ventilation_panel is not None:
|
||
if self._set_checkbox_field("Вентиляционная панель", rack_data.ventilation_panel):
|
||
results["checkboxes_set"] += 1
|
||
|
||
logger.debug(f"Fill results: {results}")
|
||
return results
|
||
|
||
def should_be_toolbar_buttons(self) -> None:
|
||
"""
|
||
Проверяет наличие и функциональность кнопок тулбара.
|
||
|
||
Raises:
|
||
AssertionError: Если кнопки недоступны или подсказки неверны
|
||
"""
|
||
|
||
logger.debug("Checking toolbar buttons...")
|
||
|
||
# Проверяем новые кнопки тулбара
|
||
self.toolbar.check_button_visibility("replace")
|
||
self.toolbar.check_button_tooltip("replace", "Переместить")
|
||
|
||
self.toolbar.check_button_visibility("done")
|
||
self.toolbar.check_button_tooltip("done", "Сохранить")
|
||
|
||
self.toolbar.check_button_visibility("close")
|
||
self.toolbar.check_button_tooltip("close", "Отменить")
|
||
|
||
self.toolbar.check_button_visibility("remove")
|
||
self.toolbar.check_button_tooltip("remove", "Удалить") |