e-nms_qa_automation/forms/edit_rack_form.py

650 lines
27 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 time
from dataclasses import dataclass
from typing import Optional, List, Dict, 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 components.base_component import BaseComponent
from components.dropdown_list_component import DropdownList
logger = get_logger("EDIT_RACK_FORM")
logger.setLevel("INFO")
@dataclass
class EditRackFormData:
"""Класс для хранения данных редактируемой стойки."""
# Основные поля
name: str = ""
serial: str = ""
inventory: str = ""
comment: str = ""
allocated_power: str = ""
# Combobox поля
cable_entry: str = ""
state: str = ""
depth: str = ""
usize: str = ""
# Combobox поля (не заполняемые)
owner: str = ""
service_org: str = ""
project: str = ""
# Checkbox поле
ventilation_panel: Optional[bool] = None
class EditRackForm(BaseComponent):
"""Компонент для работы с формой редактирования стойки в модальном окне."""
# Маппинг текстовых полей
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.EDIT_RACK_FORM_FIELD_NAME,
"Комментарий": RackLocators.EDIT_RACK_FORM_FIELD_COMMENT,
"Серийный номер": RackLocators.EDIT_RACK_FORM_FIELD_SERIAL,
"Инвентарный номер": RackLocators.EDIT_RACK_FORM_FIELD_INVENTORY,
"Выделенная мощность (Вт/ВА)": RackLocators.EDIT_RACK_FORM_FIELD_POWER,
}
# Локаторы для combobox полей (из RackLocators)
COMBOBOX_FIELDS_LOCATORS = {
"Ввод кабеля": RackLocators.EDIT_RACK_FORM_SELECT_CABLE_INPUT,
"Состояние": RackLocators.EDIT_RACK_FORM_SELECT_CONDITION_TYPE,
"Глубина (мм)": RackLocators.EDIT_RACK_FORM_SELECT_DEPTH,
"Высота в юнитах": RackLocators.EDIT_RACK_FORM_SELECT_USIZE,
"Владелец": RackLocators.EDIT_RACK_FORM_SELECT_OWNER,
"Обслуживающая организация": RackLocators.EDIT_RACK_FORM_SELECT_SERVICE_PROVIDER,
"Проект/Титул": RackLocators.EDIT_RACK_FORM_SELECT_PROJECT,
}
# Локатор для чекбокса вентиляционной панели
CHECKBOX_VENTILATION = RackLocators.EDIT_RACK_FORM_CHECKBOX_VENTILATION
def __init__(self, page: Page) -> None:
"""Инициализирует компонент формы редактирования стойки.
Args:
page: Экземпляр страницы Playwright
"""
super().__init__(page)
self.page = page
self.content_items = {}
self.available_fields = None
# Инициализация полей формы
self._init_form_fields()
def _init_form_fields(self) -> None:
"""Инициализирует все поля формы редактирования."""
# Получаем доступные поля формы
container_locator = self.page.locator(RackLocators.EDIT_RACK_FORM)
self.available_fields = self.get_input_fields_locators(container_locator)
self._init_text_fields()
self._init_combobox_fields()
self._init_checkbox_fields()
def _init_text_fields(self) -> None:
"""Инициализирует текстовые поля формы."""
for field_label, (attr_name, 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():
# Создаем TextInput для поля
field_input = TextInput(self.page, element, widget_name)
self.content_items[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, (attr_name, 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():
# Для combobox создаем TextInput для клика
field_input = TextInput(self.page, element, input_name)
self.content_items[input_name] = field_input
# Добавляем DropdownList для выбора значений
self.content_items[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(self.CHECKBOX_VENTILATION).first
if checkbox_input.count() == 0:
logger.debug("Ventilation panel checkbox not found")
return
# Импортируем Checkbox только здесь чтобы избежать циклических импортов
from elements.checkbox_element import Checkbox
checkbox = Checkbox(self.page, checkbox_input, "ventilation_panel")
self.content_items["ventilation_checkbox"] = checkbox
logger.debug("Initialized ventilation panel checkbox")
def clear_field(self, field_name: str) -> None:
"""Очищает указанное поле.
Args:
field_name: Название поля для очистки
"""
logger.debug(f"Clearing field: '{field_name}'")
# Проверяем, не является ли поле чекбоксом
if field_name == "Вентиляционная панель":
logger.debug(f"Field '{field_name}' is a checkbox, skipping clear operation")
return
# Получаем локатор поля
locator = None
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
locator = self.COMBOBOX_FIELDS_LOCATORS[field_name]
elif field_name in self.TEXT_FIELDS_LOCATORS:
locator = self.TEXT_FIELDS_LOCATORS[field_name]
else:
logger.warning(f"Unknown field: {field_name}")
return
field_element = self.page.locator(locator).first
if field_element.count() == 0:
logger.debug(f"Field '{field_name}' not found")
return
# Для текстовых полей
if field_name in self.TEXT_FIELDS_LOCATORS:
try:
field_element.click()
field_element.page.keyboard.press("Control+A")
field_element.page.keyboard.press("Backspace")
self.wait_for_timeout(200)
logger.debug(f"Text field '{field_name}' cleared")
except Exception as e:
logger.debug(f"Could not clear text field '{field_name}': {e}")
return
# Для combobox полей
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
# Поднимаемся до родительского контейнера
parent_container = field_element.locator(
"xpath=ancestor::div[contains(@class, 'v-input')]"
).first
if parent_container.count() == 0:
logger.debug(f"Parent container not found for field '{field_name}'")
return
# Ищем кнопку очистки (крестик)
clear_button = parent_container.locator(
".v-input__icon--clear button, .v-input__icon--append button, i.mdi-close-circle, i.mdi-close"
).first
if clear_button.count() > 0 and clear_button.is_visible():
clear_button.click()
self.wait_for_timeout(300)
logger.debug(f"Combobox field '{field_name}' cleared")
else:
logger.debug(f"Clear button not found for field '{field_name}'")
def get_content_item(self, item_name: str) -> Any:
"""Возвращает элемент контента по имени.
Args:
item_name: Имя элемента
Returns:
Элемент или None если не найден
"""
return self.content_items.get(item_name)
def _scroll_to_element_in_dropdown(self, value: str) -> bool:
"""Скроллит выпадающий список до элемента с нужным текстом используя playwright.
Args:
value: Текст для поиска
Returns:
bool: True если элемент найден, False в противном случае
"""
logger.debug(f"Scrolling to find element with text: '{value}'")
# Получаем активное выпадающее меню
dropdown_menu = self.page.locator("div.menuable__content__active").first
if dropdown_menu.count() == 0:
logger.error("Active dropdown menu not found")
return False
max_attempts = 10
attempts = 0
while attempts < max_attempts:
# Получаем все видимые элементы списка
visible_items = dropdown_menu.locator("a.v-list__tile, div[role='listitem']").all()
if visible_items:
# Проверяем каждый видимый элемент
for item in visible_items:
item_text = item.text_content() or ""
if value in item_text:
logger.debug(f"Found element with text '{value}'")
# Скроллим до элемента
item.scroll_into_view_if_needed()
self.wait_for_timeout(300)
return True
# Если элемент не найден, скроллим до последнего видимого элемента
last_item = visible_items[-1]
last_item_text = last_item.text_content() or ""
logger.debug(f"Scrolling to last visible item: '{last_item_text}'")
last_item.scroll_into_view_if_needed()
self.wait_for_timeout(500)
else:
# Если нет видимых элементов, скроллим вниз
dropdown_menu.evaluate("(el) => el.scrollTop += 200")
self.wait_for_timeout(300)
attempts += 1
logger.debug(f"Scroll attempt {attempts}/{max_attempts}")
logger.warning(f"Element with text '{value}' not found after {max_attempts} scroll attempts")
return False
def fill_rack_data(self, rack_data: EditRackFormData) -> Dict[str, int]:
"""Заполняет поля формы редактирования стойки.
Args:
rack_data: Данные для заполнения
Returns:
Словарь с результатами заполнения
"""
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)
logger.info(f"Filled {results['text_fields_filled']} text fields, "
f"{results['combobox_fields_filled']} combobox fields, "
f"{results['checkboxes_set']} checkboxes")
return results
def _fill_text_fields(self, rack_data: EditRackFormData, results: Dict[str, int]) -> 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[str, int]
) -> 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.debug(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: EditRackFormData, results: Dict[str, int]) -> 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[str, int]
) -> None:
"""Заполняет одно combobox поле.
Args:
field_label: Метка поля
input_name: Имя поля ввода
list_name: Имя списка
value: Значение для выбора
results: Словарь с результатами
"""
try:
combobox_field = self.get_content_item(input_name)
if not combobox_field:
logger.warning(f"Field '{field_label}' input not found")
return
# Кликаем для открытия выпадающего списка
combobox_field.click(force=True)
self.wait_for_timeout(1000)
# Скроллим до нужного элемента
if not self._scroll_to_element_in_dropdown(value):
logger.error(f"Could not find element with text '{value}' after scrolling")
# Закрываем выпадающий список кликом вне
self.page.mouse.click(10, 10)
self.wait_for_timeout(300)
return
# Получаем активное выпадающее меню
dropdown_menu = self.page.locator("div.menuable__content__active").first
# Ищем элемент с нужным текстом
item_locator = dropdown_menu.locator(f"a.v-list__tile:has-text('{value}')").first
if item_locator.count() == 0:
item_locator = dropdown_menu.locator(f"span:has-text('{value}')").first
if item_locator.count() == 0:
item_locator = dropdown_menu.locator(f"div[role='listitem']:has-text('{value}')").first
if item_locator.count() > 0:
# Убеждаемся что элемент видим и кликаем
item_locator.scroll_into_view_if_needed()
self.wait_for_timeout(300)
item_locator.click()
results["combobox_fields_filled"] += 1
logger.debug(f"Field '{field_label}' set: '{value}'")
# Небольшая пауза после выбора
self.wait_for_timeout(500)
else:
logger.error(f"Item with text '{value}' not found in dropdown for field '{field_label}'")
# Закрываем выпадающий список кликом вне
self.page.mouse.click(10, 10)
self.wait_for_timeout(300)
except Exception as e:
logger.error(f"Error filling combobox '{field_label}': {e}")
self.page.mouse.click(10, 10)
def _set_checkbox(self, rack_data: EditRackFormData, results: Dict[str, int]) -> None:
"""Устанавливает чекбокс.
Args:
rack_data: Данные для заполнения
results: Словарь с результатами
"""
if rack_data.ventilation_panel is None:
return
try:
checkbox = self.get_content_item("ventilation_checkbox")
if not checkbox:
logger.warning("Ventilation panel checkbox not found")
return
if rack_data.ventilation_panel:
checkbox.check(force=True)
logger.debug("Ventilation panel checkbox checked")
else:
checkbox.uncheck(force=True)
logger.debug("Ventilation panel checkbox unchecked")
results["checkboxes_set"] += 1
except Exception as e:
logger.error(f"Error setting checkbox: {e}")
def is_field_highlighted_as_error(self, field_name: str) -> bool:
"""Проверяет, подсвечено ли поле как ошибочное.
Args:
field_name: Название поля для проверки
Returns:
bool: True если поле подсвечено ошибкой, False в противном случае
"""
# Для чекбокса проверка ошибок не применяется
if field_name == "Вентиляционная панель":
return False
# Проверяем в текстовых полях
if field_name in self.TEXT_FIELDS_LOCATORS:
locator = self.TEXT_FIELDS_LOCATORS[field_name]
field_element = self.page.locator(locator).first
if field_element.count() == 0:
logger.debug(f"Field '{field_name}' not found")
return False
# Поднимаемся до родительского контейнера с классом v-input
parent_input = field_element.locator("xpath=ancestor::div[contains(@class, 'v-input')]").first
if parent_input.count() > 0:
# Проверяем наличие класса ошибки
class_attr = parent_input.get_attribute("class") or ""
is_error = "v-input--error" in class_attr or "error--text" in class_attr
logger.debug(f"Field '{field_name}' error state: {is_error}, classes: {class_attr}")
return is_error
# Проверяем в combobox полях
elif field_name in self.COMBOBOX_FIELDS_LOCATORS:
locator = self.COMBOBOX_FIELDS_LOCATORS[field_name]
field_element = self.page.locator(locator).first
if field_element.count() == 0:
logger.debug(f"Field '{field_name}' not found")
return False
# Поднимаемся до родительского контейнера с классом v-input
parent_input = field_element.locator("xpath=ancestor::div[contains(@class, 'v-input')]").first
if parent_input.count() > 0:
# Проверяем наличие класса ошибки
class_attr = parent_input.get_attribute("class") or ""
is_error = "v-input--error" in class_attr or "error--text" in class_attr
logger.debug(f"Field '{field_name}' error state: {is_error}, classes: {class_attr}")
return is_error
return False
def verify_required_fields_highlighted(self, field_names: List[str]) -> Dict[str, bool]:
"""Проверяет, что указанные поля подсвечены как обязательные (с ошибкой).
Args:
field_names: Список названий полей для проверки
Returns:
Словарь с результатами проверки {field_name: is_highlighted}
"""
results = {}
for field_name in field_names:
results[field_name] = self.is_field_highlighted_as_error(field_name)
logger.debug(f"Field '{field_name}' highlighted: {results[field_name]}")
return results
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
"""Ожидает появления подсветки ошибки на поле.
Args:
field_name: Название поля
timeout: Таймаут в миллисекундах
Returns:
bool: True если ошибка появилась, False в противном случае
"""
# Для чекбокса не ждем ошибок
if field_name == "Вентиляционная панель":
return False
start_time = time.time()
while (time.time() - start_time) * 1000 < timeout:
if self.is_field_highlighted_as_error(field_name):
return True
self.wait_for_timeout(200)
return False
def get_field_value(self, field_name: str) -> Optional[str]:
"""Получает значение поля.
Args:
field_name: Название поля
Returns:
Значение поля или None если поле не найдено
"""
# Для чекбокса
if field_name == "Вентиляционная панель":
checkbox = self.get_content_item("ventilation_checkbox")
if checkbox:
return str(checkbox.is_checked())
return None
# Для текстовых полей
if field_name in self.TEXT_FIELDS_LOCATORS:
for field_label, (attr_name, widget_name) in self.TEXT_FIELDS_MAPPING.items():
if attr_name == field_name or field_label == field_name:
input_field = self.get_content_item(widget_name)
if input_field:
return input_field.get_input_value()
return None
# Для combobox полей
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_name)
if not locator:
# Пробуем найти по атрибуту
for field_label, (attr_name, input_name, _) in self.COMBOBOX_FIELDS_MAPPING.items():
if attr_name == field_name or field_label == field_name:
input_field = self.get_content_item(input_name)
if input_field:
# Получаем текст из поля
element = input_field.element
selections = element.locator("xpath=ancestor::div[contains(@class, 'v-select__selections')]").first
if selections.count() > 0:
value_span = selections.locator("span").first
return value_span.text_content() or ""
return None
element = self.page.locator(locator).first
if element.count() > 0:
selections = element.locator("xpath=ancestor::div[contains(@class, 'v-select__selections')]").first
if selections.count() > 0:
value_span = selections.locator("span").first
return value_span.text_content() or ""
return None