e-nms_qa_automation/forms/create_rack_form.py

521 lines
21 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 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("CREATE_RACK_FORM")
logger.setLevel("INFO")
@dataclass
class CreateRackData:
"""Класс для хранения данных создаваемой стойки."""
# Основные поля
name: str = ""
serial: str = ""
inventory: str = ""
comment: str = ""
# Combobox поля
cable_entry: str = ""
state: str = ""
depth: str = ""
usize: str = ""
# Combobox поля (не заполняемые)
owner: str = ""
service_org: str = ""
project: str = ""
class CreateRackForm(BaseComponent):
"""Компонент для работы с формой создания стойки."""
# Маппинг текстовых полей
TEXT_FIELDS_MAPPING = {
"Имя": ("name", "name_input"),
"Комментарий": ("comment", "comment_input"),
"Серийный номер": ("serial", "serial_input"),
"Инвентарный номер": ("inventory", "inventory_input"),
}
# Маппинг полей для заполнения combobox полей
COMBOBOX_FIELDS_MAPPING = {
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
"Состояние": ("state", "state_input", "state_list"),
"Высота в юнитах": ("usize", "usize_input", "usize_list"),
"Глубина (мм)": ("depth", "depth_input", "depth_list"),
"Владелец": ("owner", "owner_input", "owner_list"),
"Обслуживающая организация": ("service_org", "service_input", "service_list"),
"Проект/Титул": ("project", "project_input", "project_list")
}
# Локаторы для текстовых полей (из RackLocators)
TEXT_FIELDS_LOCATORS = {
"Имя": RackLocators.CREATE_RACK_FORM_FIELD_NAME,
"Комментарий": RackLocators.CREATE_RACK_FORM_FIELD_COMMENT,
"Серийный номер": RackLocators.CREATE_RACK_FORM_FIELD_SERIAL,
"Инвентарный номер": RackLocators.CREATE_RACK_FORM_FIELD_INVENTORY,
}
# Локаторы для combobox полей (из RackLocators)
COMBOBOX_FIELDS_LOCATORS = {
"Высота в юнитах": RackLocators.CREATE_RACK_FORM_SELECT_USIZE,
"Глубина (мм)": RackLocators.CREATE_RACK_FORM_SELECT_DEPTH,
"Ввод кабеля": RackLocators.CREATE_RACK_FORM_SELECT_CABLE_INPUT,
"Состояние": RackLocators.CREATE_RACK_FORM_SELECT_CONDITION_TYPE,
"Владелец": RackLocators.CREATE_RACK_FORM_SELECT_OWNER,
"Обслуживающая организация": RackLocators.CREATE_RACK_FORM_SELECT_SERVICE_PROVIDER,
"Проект/Титул": RackLocators.CREATE_RACK_FORM_SELECT_PROJECT,
}
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.CREATE_RACK_FORM_CONTAINER).nth(1)
self.available_fields = self.get_input_fields_locators(container_locator)
self._init_text_fields()
self._init_combobox_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 clear_field(self, field_name: str) -> None:
"""Очищает указанное поле.
Args:
field_name: Название поля для очистки
"""
logger.debug(f"Clearing field: '{field_name}'")
# Получаем локатор поля
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
last_item_text = ""
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: CreateRackData) -> Dict[str, int]:
"""Заполняет поля формы создания стойки.
Args:
rack_data: Данные для заполнения
Returns:
Словарь с результатами заполнения
"""
results = {
"text_fields_filled": 0,
"combobox_fields_filled": 0,
}
self._fill_text_fields(rack_data, results)
self._fill_combobox_fields(rack_data, results)
logger.info(f"Filled {results['text_fields_filled']} text fields and "
f"{results['combobox_fields_filled']} combobox fields")
return results
def _fill_text_fields(self, rack_data: CreateRackData, 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: CreateRackData, 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 is_field_highlighted_as_error(self, field_name: str) -> bool:
"""Проверяет, подсвечено ли поле как ошибочное.
Args:
field_name: Название поля для проверки
Returns:
bool: True если поле подсвечено ошибкой, 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 в противном случае
"""
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