refactor: унификация работы с формами создания и редактирования стоек
- Добавлен базовый класс BaseRackForm с общей логикой для работы с формами - Вынесена общая функциональность по заполнению полей, очистке и проверке ошибок - Упрощены классы CreateRackForm и EditRackForm за счет наследования от BaseRackForm - Обновлены зависимые компоненты (create_element_frame, edit_rack_maker) - Исправлены тесты создания стойки с учетом новой архитектурыra6/create_rack
parent
4fff4835f1
commit
0295852986
|
|
@ -1,43 +1,23 @@
|
||||||
|
# forms/rack_create_form.py
|
||||||
"""Модуль для работы с формой создания стойки."""
|
"""Модуль для работы с формой создания стойки."""
|
||||||
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Dict, Any
|
from typing import Dict
|
||||||
from playwright.sync_api import Page
|
from playwright.sync_api import Page
|
||||||
from tools.logger import get_logger
|
from tools.logger import get_logger
|
||||||
from locators.rack_locators import RackLocators
|
from locators.rack_locators import RackLocators
|
||||||
from elements.text_input_element import TextInput
|
from forms.base_rack_form import BaseRackForm, BaseRackData
|
||||||
from components.base_component import BaseComponent
|
|
||||||
from components.dropdown_list_component import DropdownList
|
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("CREATE_RACK_FORM")
|
logger = get_logger("CREATE_RACK_FORM")
|
||||||
logger.setLevel("INFO")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CreateRackData:
|
class CreateRackData(BaseRackData):
|
||||||
"""Класс для хранения данных создаваемой стойки."""
|
"""Класс для хранения данных создаваемой стойки."""
|
||||||
|
pass # Используем все поля из базового класса
|
||||||
# Основные поля
|
|
||||||
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):
|
class CreateRackForm(BaseRackForm):
|
||||||
"""Компонент для работы с формой создания стойки."""
|
"""Компонент для работы с формой создания стойки."""
|
||||||
|
|
||||||
# Маппинг текстовых полей
|
# Маппинг текстовых полей
|
||||||
|
|
@ -48,7 +28,7 @@ class CreateRackForm(BaseComponent):
|
||||||
"Инвентарный номер": ("inventory", "inventory_input"),
|
"Инвентарный номер": ("inventory", "inventory_input"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Маппинг полей для заполнения combobox полей
|
# Маппинг combobox полей
|
||||||
COMBOBOX_FIELDS_MAPPING = {
|
COMBOBOX_FIELDS_MAPPING = {
|
||||||
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
|
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
|
||||||
"Состояние": ("state", "state_input", "state_list"),
|
"Состояние": ("state", "state_input", "state_list"),
|
||||||
|
|
@ -59,7 +39,7 @@ class CreateRackForm(BaseComponent):
|
||||||
"Проект/Титул": ("project", "project_input", "project_list")
|
"Проект/Титул": ("project", "project_input", "project_list")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для текстовых полей (из RackLocators)
|
# Локаторы для текстовых полей
|
||||||
TEXT_FIELDS_LOCATORS = {
|
TEXT_FIELDS_LOCATORS = {
|
||||||
"Имя": RackLocators.CREATE_RACK_FORM_FIELD_NAME,
|
"Имя": RackLocators.CREATE_RACK_FORM_FIELD_NAME,
|
||||||
"Комментарий": RackLocators.CREATE_RACK_FORM_FIELD_COMMENT,
|
"Комментарий": RackLocators.CREATE_RACK_FORM_FIELD_COMMENT,
|
||||||
|
|
@ -67,7 +47,7 @@ class CreateRackForm(BaseComponent):
|
||||||
"Инвентарный номер": RackLocators.CREATE_RACK_FORM_FIELD_INVENTORY,
|
"Инвентарный номер": RackLocators.CREATE_RACK_FORM_FIELD_INVENTORY,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для combobox полей (из RackLocators)
|
# Локаторы для combobox полей
|
||||||
COMBOBOX_FIELDS_LOCATORS = {
|
COMBOBOX_FIELDS_LOCATORS = {
|
||||||
"Высота в юнитах": RackLocators.CREATE_RACK_FORM_SELECT_USIZE,
|
"Высота в юнитах": RackLocators.CREATE_RACK_FORM_SELECT_USIZE,
|
||||||
"Глубина (мм)": RackLocators.CREATE_RACK_FORM_SELECT_DEPTH,
|
"Глубина (мм)": RackLocators.CREATE_RACK_FORM_SELECT_DEPTH,
|
||||||
|
|
@ -79,230 +59,11 @@ class CreateRackForm(BaseComponent):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, page: Page) -> None:
|
def __init__(self, page: Page) -> None:
|
||||||
"""Инициализирует компонент формы создания стойки.
|
"""Инициализирует компонент формы создания стойки."""
|
||||||
|
super().__init__(page, RackLocators.CREATE_RACK_FORM_CONTAINER)
|
||||||
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]:
|
def fill_rack_data(self, rack_data: CreateRackData) -> Dict[str, int]:
|
||||||
"""Заполняет поля формы создания стойки.
|
"""Заполняет поля формы создания стойки."""
|
||||||
|
|
||||||
Args:
|
|
||||||
rack_data: Данные для заполнения
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Словарь с результатами заполнения
|
|
||||||
"""
|
|
||||||
|
|
||||||
results = {
|
results = {
|
||||||
"text_fields_filled": 0,
|
"text_fields_filled": 0,
|
||||||
"combobox_fields_filled": 0,
|
"combobox_fields_filled": 0,
|
||||||
|
|
@ -314,207 +75,3 @@ class CreateRackForm(BaseComponent):
|
||||||
logger.info(f"Filled {results['text_fields_filled']} text fields and "
|
logger.info(f"Filled {results['text_fields_filled']} text fields and "
|
||||||
f"{results['combobox_fields_filled']} combobox fields")
|
f"{results['combobox_fields_filled']} combobox fields")
|
||||||
return results
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,28 @@
|
||||||
"""Модуль для работы с формой редактирования стойки в модальном окне."""
|
# forms/rack_edit_form.py
|
||||||
|
"""Модуль для работы с формой редактирования стойки."""
|
||||||
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, Dict
|
||||||
from playwright.sync_api import Page
|
from playwright.sync_api import Page
|
||||||
from tools.logger import get_logger
|
from tools.logger import get_logger
|
||||||
from locators.rack_locators import RackLocators
|
from locators.rack_locators import RackLocators
|
||||||
from elements.text_input_element import TextInput
|
from forms.base_rack_form import BaseRackForm, BaseRackData
|
||||||
from components.base_component import BaseComponent
|
|
||||||
from components.dropdown_list_component import DropdownList
|
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("EDIT_RACK_FORM")
|
logger = get_logger("EDIT_RACK_FORM")
|
||||||
logger.setLevel("INFO")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EditRackFormData:
|
class EditRackData(BaseRackData):
|
||||||
"""Класс для хранения данных редактируемой стойки."""
|
"""Класс для хранения данных редактируемой стойки."""
|
||||||
|
|
||||||
# Основные поля
|
# Дополнительное поле для формы редактирования
|
||||||
name: str = ""
|
|
||||||
serial: str = ""
|
|
||||||
inventory: str = ""
|
|
||||||
comment: str = ""
|
|
||||||
allocated_power: 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
|
ventilation_panel: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class EditRackForm(BaseComponent):
|
class EditRackForm(BaseRackForm):
|
||||||
"""Компонент для работы с формой редактирования стойки в модальном окне."""
|
"""Компонент для работы с формой редактирования стойки."""
|
||||||
|
|
||||||
# Маппинг текстовых полей
|
# Маппинг текстовых полей
|
||||||
TEXT_FIELDS_MAPPING = {
|
TEXT_FIELDS_MAPPING = {
|
||||||
|
|
@ -53,7 +33,7 @@ class EditRackForm(BaseComponent):
|
||||||
"Выделенная мощность (Вт/ВА)": ("allocated_power", "power_input"),
|
"Выделенная мощность (Вт/ВА)": ("allocated_power", "power_input"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Маппинг полей для заполнения combobox полей
|
# Маппинг combobox полей
|
||||||
COMBOBOX_FIELDS_MAPPING = {
|
COMBOBOX_FIELDS_MAPPING = {
|
||||||
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
|
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
|
||||||
"Состояние": ("state", "state_input", "state_list"),
|
"Состояние": ("state", "state_input", "state_list"),
|
||||||
|
|
@ -64,7 +44,12 @@ class EditRackForm(BaseComponent):
|
||||||
"Проект/Титул": ("project", "project_input", "project_list")
|
"Проект/Титул": ("project", "project_input", "project_list")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для текстовых полей (из RackLocators)
|
# Маппинг checkbox полей
|
||||||
|
CHECKBOX_FIELDS_MAPPING = {
|
||||||
|
"Вентиляционная панель": ("ventilation_panel", "ventilation_checkbox"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Локаторы для текстовых полей
|
||||||
TEXT_FIELDS_LOCATORS = {
|
TEXT_FIELDS_LOCATORS = {
|
||||||
"Имя": RackLocators.EDIT_RACK_FORM_FIELD_NAME,
|
"Имя": RackLocators.EDIT_RACK_FORM_FIELD_NAME,
|
||||||
"Комментарий": RackLocators.EDIT_RACK_FORM_FIELD_COMMENT,
|
"Комментарий": RackLocators.EDIT_RACK_FORM_FIELD_COMMENT,
|
||||||
|
|
@ -73,7 +58,7 @@ class EditRackForm(BaseComponent):
|
||||||
"Выделенная мощность (Вт/ВА)": RackLocators.EDIT_RACK_FORM_FIELD_POWER,
|
"Выделенная мощность (Вт/ВА)": RackLocators.EDIT_RACK_FORM_FIELD_POWER,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для combobox полей (из RackLocators)
|
# Локаторы для combobox полей
|
||||||
COMBOBOX_FIELDS_LOCATORS = {
|
COMBOBOX_FIELDS_LOCATORS = {
|
||||||
"Ввод кабеля": RackLocators.EDIT_RACK_FORM_SELECT_CABLE_INPUT,
|
"Ввод кабеля": RackLocators.EDIT_RACK_FORM_SELECT_CABLE_INPUT,
|
||||||
"Состояние": RackLocators.EDIT_RACK_FORM_SELECT_CONDITION_TYPE,
|
"Состояние": RackLocators.EDIT_RACK_FORM_SELECT_CONDITION_TYPE,
|
||||||
|
|
@ -84,262 +69,17 @@ class EditRackForm(BaseComponent):
|
||||||
"Проект/Титул": RackLocators.EDIT_RACK_FORM_SELECT_PROJECT,
|
"Проект/Титул": RackLocators.EDIT_RACK_FORM_SELECT_PROJECT,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локатор для чекбокса вентиляционной панели
|
# Локаторы для checkbox полей
|
||||||
CHECKBOX_VENTILATION = RackLocators.EDIT_RACK_FORM_CHECKBOX_VENTILATION
|
CHECKBOX_FIELDS_LOCATORS = {
|
||||||
|
"Вентиляционная панель": RackLocators.EDIT_RACK_FORM_CHECKBOX_VENTILATION,
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, page: Page) -> None:
|
def __init__(self, page: Page) -> None:
|
||||||
"""Инициализирует компонент формы редактирования стойки.
|
"""Инициализирует компонент формы редактирования стойки."""
|
||||||
|
super().__init__(page, RackLocators.EDIT_RACK_FORM)
|
||||||
|
|
||||||
Args:
|
def fill_rack_data(self, rack_data: EditRackData) -> Dict[str, int]:
|
||||||
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 = {
|
results = {
|
||||||
"text_fields_filled": 0,
|
"text_fields_filled": 0,
|
||||||
"combobox_fields_filled": 0,
|
"combobox_fields_filled": 0,
|
||||||
|
|
@ -348,302 +88,9 @@ class EditRackForm(BaseComponent):
|
||||||
|
|
||||||
self._fill_text_fields(rack_data, results)
|
self._fill_text_fields(rack_data, results)
|
||||||
self._fill_combobox_fields(rack_data, results)
|
self._fill_combobox_fields(rack_data, results)
|
||||||
self._set_checkbox(rack_data, results)
|
self._fill_checkbox_fields(rack_data, results)
|
||||||
|
|
||||||
logger.info(f"Filled {results['text_fields_filled']} text fields, "
|
logger.info(f"Filled {results['text_fields_filled']} text fields, "
|
||||||
f"{results['combobox_fields_filled']} combobox fields, "
|
f"{results['combobox_fields_filled']} combobox fields, "
|
||||||
f"{results['checkboxes_set']} checkboxes")
|
f"{results['checkboxes_set']} checkboxes")
|
||||||
return results
|
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
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""Модуль фрейма создания дочернего элемента."""
|
"""Модуль фрейма создания дочернего элемента."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
from playwright.sync_api import Page, Locator
|
from playwright.sync_api import Page, Locator
|
||||||
from tools.logger import get_logger
|
from tools.logger import get_logger
|
||||||
from locators.rack_locators import RackLocators
|
from locators.rack_locators import RackLocators
|
||||||
|
|
@ -9,6 +10,7 @@ from components.alert_component import AlertComponent
|
||||||
from components.base_component import BaseComponent
|
from components.base_component import BaseComponent
|
||||||
from components.toolbar_component import ToolbarComponent
|
from components.toolbar_component import ToolbarComponent
|
||||||
from components_derived.selection_bar_component import SelectionBarComponent
|
from components_derived.selection_bar_component import SelectionBarComponent
|
||||||
|
from forms.create_rack_form import CreateRackForm, CreateRackData
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("CREATE_ELEMENT_FRAME")
|
logger = get_logger("CREATE_ELEMENT_FRAME")
|
||||||
|
|
@ -27,6 +29,9 @@ class CreateElementFrame(BaseComponent):
|
||||||
"""
|
"""
|
||||||
super().__init__(page)
|
super().__init__(page)
|
||||||
|
|
||||||
|
# Инициализация формы создания стойки
|
||||||
|
self.rack_form = CreateRackForm(page)
|
||||||
|
|
||||||
# Инициализация компонентов
|
# Инициализация компонентов
|
||||||
self.toolbar = ToolbarComponent(page, "Создать дочерний элемент в")
|
self.toolbar = ToolbarComponent(page, "Создать дочерний элемент в")
|
||||||
self.selection_bar = SelectionBarComponent(page, "Класс объекта учета")
|
self.selection_bar = SelectionBarComponent(page, "Класс объекта учета")
|
||||||
|
|
@ -46,7 +51,67 @@ class CreateElementFrame(BaseComponent):
|
||||||
self.toolbar.add_tooltip_button(add_button_locator, "add")
|
self.toolbar.add_tooltip_button(add_button_locator, "add")
|
||||||
self.toolbar.add_tooltip_button(cancel_button_locator, "cancel")
|
self.toolbar.add_tooltip_button(cancel_button_locator, "cancel")
|
||||||
|
|
||||||
# Действия:
|
# Делегирование методов форме создания стойки
|
||||||
|
|
||||||
|
def fill_rack_data(self, rack_data: CreateRackData) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Заполняет поля формы создания стойки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rack_data: Данные для заполнения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Словарь с результатами заполнения
|
||||||
|
"""
|
||||||
|
return self.rack_form.fill_rack_data(rack_data)
|
||||||
|
|
||||||
|
def clear_field(self, field_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Очищает указанное поле формы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля для очистки
|
||||||
|
"""
|
||||||
|
self.rack_form.clear_field(field_name)
|
||||||
|
|
||||||
|
def get_field_value(self, field_name: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Получает значение поля формы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Значение поля или None если поле не найдено
|
||||||
|
"""
|
||||||
|
return self.rack_form.get_field_value(field_name)
|
||||||
|
|
||||||
|
def is_field_highlighted_as_error(self, field_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, подсвечено ли поле как ошибочное.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля для проверки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если поле подсвечено ошибкой
|
||||||
|
"""
|
||||||
|
return self.rack_form.is_field_highlighted_as_error(field_name)
|
||||||
|
|
||||||
|
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
|
||||||
|
"""
|
||||||
|
Ожидает появления подсветки ошибки на поле.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля
|
||||||
|
timeout: Таймаут в миллисекундах
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если ошибка появилась
|
||||||
|
"""
|
||||||
|
return self.rack_form.wait_for_field_error(field_name, timeout)
|
||||||
|
|
||||||
|
# Оригинальные методы фрейма
|
||||||
|
|
||||||
def clear_combobox_field(self, field_name: str) -> None:
|
def clear_combobox_field(self, field_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -192,22 +257,9 @@ class CreateElementFrame(BaseComponent):
|
||||||
AssertionError: Если поле не подсвечено ошибкой
|
AssertionError: Если поле не подсвечено ошибкой
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Checking field '{field_name}' for error highlighting...")
|
logger.debug(f"Checking field '{field_name}' for error highlighting...")
|
||||||
|
assert self.is_field_highlighted_as_error(field_name), (
|
||||||
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
|
f"Field '{field_name}' is not highlighted as error"
|
||||||
fields_locators = self.get_input_fields_locators(container_locator)
|
|
||||||
field_container = fields_locators.get(field_name)
|
|
||||||
|
|
||||||
if not field_container:
|
|
||||||
raise ValueError(f"Field '{field_name}' not found in form")
|
|
||||||
|
|
||||||
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
|
|
||||||
has_error = error_elements.count() > 0
|
|
||||||
|
|
||||||
assert has_error, (
|
|
||||||
f"Field '{field_name}' has no elements with error classes. "
|
|
||||||
f"Expected to find elements matching: {SelectionBarLocators.ERROR_CSS_SELECTORS}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"Field '{field_name}' is correctly highlighted with error color")
|
logger.debug(f"Field '{field_name}' is correctly highlighted with error color")
|
||||||
|
|
||||||
def check_field_error_not_highlighted(self, field_name: str) -> None:
|
def check_field_error_not_highlighted(self, field_name: str) -> None:
|
||||||
|
|
@ -222,22 +274,9 @@ class CreateElementFrame(BaseComponent):
|
||||||
AssertionError: Если поле подсвечено ошибкой
|
AssertionError: Если поле подсвечено ошибкой
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Checking field '{field_name}' for absence of error highlighting...")
|
logger.debug(f"Checking field '{field_name}' for absence of error highlighting...")
|
||||||
|
assert not self.is_field_highlighted_as_error(field_name), (
|
||||||
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
|
f"Field '{field_name}' is incorrectly highlighted as error"
|
||||||
fields_locators = self.get_input_fields_locators(container_locator)
|
|
||||||
field_container = fields_locators.get(field_name)
|
|
||||||
|
|
||||||
if not field_container:
|
|
||||||
raise ValueError(f"Field '{field_name}' not found in form")
|
|
||||||
|
|
||||||
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
|
|
||||||
has_error = error_elements.count() > 0
|
|
||||||
|
|
||||||
assert not has_error, (
|
|
||||||
f"Field '{field_name}' has {error_elements.count()} elements with error classes. "
|
|
||||||
f"Expected no elements matching: {SelectionBarLocators.ERROR_CSS_SELECTORS}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"Field '{field_name}' correctly has no error highlighting")
|
logger.debug(f"Field '{field_name}' correctly has no error highlighting")
|
||||||
|
|
||||||
def check_object_class_selected(self, expected_class: str) -> None:
|
def check_object_class_selected(self, expected_class: str) -> None:
|
||||||
|
|
@ -296,4 +335,4 @@ class CreateElementFrame(BaseComponent):
|
||||||
self.toolbar.check_button_visibility("cancel")
|
self.toolbar.check_button_visibility("cancel")
|
||||||
self.toolbar.check_button_tooltip("cancel", "Отменить")
|
self.toolbar.check_button_tooltip("cancel", "Отменить")
|
||||||
self.toolbar.click_button("cancel")
|
self.toolbar.click_button("cancel")
|
||||||
self.wait_for_timeout(500)
|
self.wait_for_timeout(500)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# makers/edit_rack_maker.py
|
||||||
"""Модуль для работы с модальным окном редактирования стойки."""
|
"""Модуль для работы с модальным окном редактирования стойки."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
@ -9,17 +10,16 @@ from components.modal_window_component import ModalWindowComponent
|
||||||
from components.dropdown_list_component import DropdownList
|
from components.dropdown_list_component import DropdownList
|
||||||
from components.confirm_component import ConfirmComponent
|
from components.confirm_component import ConfirmComponent
|
||||||
from elements.text_input_element import TextInput
|
from elements.text_input_element import TextInput
|
||||||
from elements.text_element import Text
|
from forms.edit_rack_form import EditRackForm, EditRackData
|
||||||
from forms.edit_rack_form import EditRackForm, EditRackFormData
|
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("EDIT_RACK_MAKER")
|
logger = get_logger("EDIT_RACK_MAKER")
|
||||||
logger.setLevel("INFO")
|
logger.setLevel("INFO")
|
||||||
|
|
||||||
|
|
||||||
# Используем EditRackFormData
|
# Re-export EditRackData for backward compatibility
|
||||||
EditRackData = EditRackFormData
|
EditRackData = EditRackData
|
||||||
|
__all__ = ['EditRackMaker', 'EditRackData']
|
||||||
|
|
||||||
class EditRackMaker(ModalWindowComponent):
|
class EditRackMaker(ModalWindowComponent):
|
||||||
"""Компонент для работы с модальным окном редактирования стойки.
|
"""Компонент для работы с модальным окном редактирования стойки.
|
||||||
|
|
@ -38,7 +38,7 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
TAB_IMAGE = "Изображение"
|
TAB_IMAGE = "Изображение"
|
||||||
TAB_SETTINGS = "Настройки"
|
TAB_SETTINGS = "Настройки"
|
||||||
|
|
||||||
# Маппинг полей для вкладки "Настройки" - оставляем только то, что специфично для модального окна
|
# Маппинг полей для вкладки "Настройки"
|
||||||
ACCESS_RULES_MAPPING = {
|
ACCESS_RULES_MAPPING = {
|
||||||
"Правила доступа для чтения": (
|
"Правила доступа для чтения": (
|
||||||
"read_access_rules", "rules_read_input", "rules_read_list"
|
"read_access_rules", "rules_read_input", "rules_read_list"
|
||||||
|
|
@ -57,7 +57,7 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Локаторы для полей правил доступа (из RackLocators)
|
# Локаторы для полей правил доступа
|
||||||
ACCESS_RULES_LOCATORS = {
|
ACCESS_RULES_LOCATORS = {
|
||||||
"Правила доступа для чтения": RackLocators.SETTINGS_READ_RULES,
|
"Правила доступа для чтения": RackLocators.SETTINGS_READ_RULES,
|
||||||
"Правила доступа для записи": RackLocators.SETTINGS_WRITE_RULES,
|
"Правила доступа для записи": RackLocators.SETTINGS_WRITE_RULES,
|
||||||
|
|
@ -210,6 +210,82 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
self.add_button(self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON), "cancel")
|
self.add_button(self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON), "cancel")
|
||||||
self.add_button(self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON), "delete")
|
self.add_button(self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON), "delete")
|
||||||
|
|
||||||
|
# Делегирование методов форме редактирования
|
||||||
|
|
||||||
|
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 not self.edit_form:
|
||||||
|
logger.error("Edit form not initialized")
|
||||||
|
return {
|
||||||
|
"text_fields_filled": 0,
|
||||||
|
"combobox_fields_filled": 0,
|
||||||
|
"checkboxes_set": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
results = self.edit_form.fill_rack_data(rack_data)
|
||||||
|
logger.info(f"Filled rack data via EditRackForm: {results}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
def clear_field(self, field_name: str) -> None:
|
||||||
|
"""Очищает указанное поле формы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля для очистки.
|
||||||
|
"""
|
||||||
|
if self.edit_form:
|
||||||
|
self.edit_form.clear_field(field_name)
|
||||||
|
|
||||||
|
def get_field_value(self, field_name: str) -> Optional[str]:
|
||||||
|
"""Получает значение поля формы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Значение поля или None если поле не найдено.
|
||||||
|
"""
|
||||||
|
if self.edit_form:
|
||||||
|
return self.edit_form.get_field_value(field_name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_field_highlighted_as_error(self, field_name: str) -> bool:
|
||||||
|
"""Проверяет, подсвечено ли поле как ошибочное.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля для проверки.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если поле подсвечено ошибкой.
|
||||||
|
"""
|
||||||
|
if self.edit_form:
|
||||||
|
return self.edit_form.is_field_highlighted_as_error(field_name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
|
||||||
|
"""Ожидает появления подсветки ошибки на поле.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Название поля.
|
||||||
|
timeout: Таймаут в миллисекундах.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если ошибка появилась.
|
||||||
|
"""
|
||||||
|
if self.edit_form:
|
||||||
|
return self.edit_form.wait_for_field_error(field_name, timeout)
|
||||||
|
return False
|
||||||
|
|
||||||
# Действия с вкладками
|
# Действия с вкладками
|
||||||
def switch_to_tab(self, tab_name: str) -> None:
|
def switch_to_tab(self, tab_name: str) -> None:
|
||||||
"""Переключается на указанную вкладку.
|
"""Переключается на указанную вкладку.
|
||||||
|
|
@ -348,11 +424,7 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
target_fields: Список целевых полей для заполнения.
|
target_fields: Список целевых полей для заполнения.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Словарь с результатами заполнения:
|
Словарь с результатами заполнения.
|
||||||
- access_rules_filled: количество добавленных пользователей
|
|
||||||
- errors: список ошибок
|
|
||||||
- fields_processed: обработанные поля
|
|
||||||
- field_stats: статистика по каждому полю
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.active_tab != self.TAB_SETTINGS:
|
if self.active_tab != self.TAB_SETTINGS:
|
||||||
|
|
@ -632,13 +704,7 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
target_fields: Список целевых полей для проверки.
|
target_fields: Список целевых полей для проверки.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Словарь с результатами проверки:
|
Словарь с результатами проверки.
|
||||||
- total_expected_fields: общее количество ожидаемых значений
|
|
||||||
- correctly_filled: количество корректно заполненных
|
|
||||||
- incorrectly_filled: количество некорректно заполненных
|
|
||||||
- field_errors: список ошибок по полям
|
|
||||||
- fields_verified: список проверенных полей
|
|
||||||
- expected_users: список ожидаемых пользователей
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.active_tab != self.TAB_SETTINGS:
|
if self.active_tab != self.TAB_SETTINGS:
|
||||||
|
|
@ -859,32 +925,6 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
save_button.click()
|
save_button.click()
|
||||||
logger.debug("Clicked done button")
|
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(
|
def verify_all_filled_fields(
|
||||||
self,
|
self,
|
||||||
|
|
@ -916,6 +956,11 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
if skip_fields is None:
|
if skip_fields is None:
|
||||||
skip_fields = []
|
skip_fields = []
|
||||||
|
|
||||||
|
if not self.edit_form:
|
||||||
|
logger.error("Edit form not initialized")
|
||||||
|
results["field_errors"].append("Edit form not initialized")
|
||||||
|
return results
|
||||||
|
|
||||||
# Проверяем текстовые поля
|
# Проверяем текстовые поля
|
||||||
self._verify_text_fields(rack_data, skip_fields, results)
|
self._verify_text_fields(rack_data, skip_fields, results)
|
||||||
|
|
||||||
|
|
@ -948,10 +993,6 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
results: Словарь с результатами для обновления.
|
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():
|
for field_label, (attr_name, field_name) in self.edit_form.TEXT_FIELDS_MAPPING.items():
|
||||||
expected_value = getattr(rack_data, attr_name, "")
|
expected_value = getattr(rack_data, attr_name, "")
|
||||||
if not expected_value or not str(expected_value).strip():
|
if not expected_value or not str(expected_value).strip():
|
||||||
|
|
@ -1014,10 +1055,6 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
results: Словарь с результатами для обновления.
|
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():
|
for field_label, (attr_name, _, _) in self.edit_form.COMBOBOX_FIELDS_MAPPING.items():
|
||||||
expected_value = getattr(rack_data, attr_name, "")
|
expected_value = getattr(rack_data, attr_name, "")
|
||||||
if not expected_value or not str(expected_value).strip():
|
if not expected_value or not str(expected_value).strip():
|
||||||
|
|
@ -1046,9 +1083,9 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actual_value = self._get_combobox_value(field_label)
|
actual_value = self.edit_form.get_field_value(field_label) or ""
|
||||||
actual_clean = actual_value.strip() if actual_value else ""
|
actual_clean = actual_value.strip()
|
||||||
expected_clean = expected_value.strip() if expected_value else ""
|
expected_clean = expected_value.strip()
|
||||||
|
|
||||||
if actual_clean == expected_clean:
|
if actual_clean == expected_clean:
|
||||||
results["correctly_filled"] += 1
|
results["correctly_filled"] += 1
|
||||||
|
|
@ -1061,40 +1098,6 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
results["not_filled"] += 1
|
results["not_filled"] += 1
|
||||||
results["field_errors"].append(f"Error checking combobox '{field_label}': {e}")
|
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(
|
def _verify_checkbox(
|
||||||
self,
|
self,
|
||||||
rack_data: EditRackData,
|
rack_data: EditRackData,
|
||||||
|
|
@ -1138,4 +1141,4 @@ class EditRackMaker(ModalWindowComponent):
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["not_filled"] += 1
|
results["not_filled"] += 1
|
||||||
results["field_errors"].append(f"Error checking checkbox: {str(e)}")
|
results["field_errors"].append(f"Error checking checkbox: {str(e)}")
|
||||||
|
|
@ -52,7 +52,7 @@ class TestCreateRack:
|
||||||
|
|
||||||
# Переходим к Объектам
|
# Переходим к Объектам
|
||||||
self.main_page.click_main_navigation_panel_item("Объекты")
|
self.main_page.click_main_navigation_panel_item("Объекты")
|
||||||
self.main_page.wait_for_timeout(1000)
|
self.main_page.wait_for_timeout(2000)
|
||||||
self.main_page.click_main_navigation_panel_item("test-zone")
|
self.main_page.click_main_navigation_panel_item("test-zone")
|
||||||
|
|
||||||
# Создаем экземпляр страницы локации
|
# Создаем экземпляр страницы локации
|
||||||
|
|
@ -122,7 +122,7 @@ class TestCreateRack:
|
||||||
|
|
||||||
# Ждем появления alert с текстом
|
# Ждем появления alert с текстом
|
||||||
expected_alert_text = f"Элемент {rack_data.name} создан"
|
expected_alert_text = f"Элемент {rack_data.name} создан"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ class TestCreateRack:
|
||||||
|
|
||||||
# Проверяем уведомление об успешном удалении
|
# Проверяем уведомление об успешном удалении
|
||||||
expected_alert_text = "Успешно удалено"
|
expected_alert_text = "Успешно удалено"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -331,7 +331,7 @@ class TestCreateRack:
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
|
|
||||||
expected_alert_text = f"Имя {rack_name} уже используется"
|
expected_alert_text = f"Имя {rack_name} уже используется"
|
||||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
|
||||||
|
|
||||||
|
|
@ -384,8 +384,8 @@ class TestCreateRack:
|
||||||
self.create_child_frame.wait_for_timeout(500)
|
self.create_child_frame.wait_for_timeout(500)
|
||||||
|
|
||||||
# Проверяем alert для высоты, глубины
|
# Проверяем alert для высоты, глубины
|
||||||
self.alert.check_alert_presence(expected_alert_text_height, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_height, timeout=7000)
|
||||||
self.alert.check_alert_presence(expected_alert_text_depth, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_depth, timeout=7000)
|
||||||
|
|
||||||
# Проверяем, закрылся ли автоматически alert для высоты, глубины
|
# Проверяем, закрылся ли автоматически alert для высоты, глубины
|
||||||
self.alert.check_alert_absence(expected_alert_text_height, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text_height, timeout=7000)
|
||||||
|
|
@ -413,7 +413,7 @@ class TestCreateRack:
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
|
|
||||||
# Проверяем alert для глубины
|
# Проверяем alert для глубины
|
||||||
self.alert.check_alert_presence(expected_alert_text_depth, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_depth, timeout=7000)
|
||||||
|
|
||||||
# Проверяем, закрылся ли автоматически alert для глубины
|
# Проверяем, закрылся ли автоматически alert для глубины
|
||||||
self.alert.check_alert_absence(expected_alert_text_depth, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text_depth, timeout=7000)
|
||||||
|
|
@ -439,7 +439,7 @@ class TestCreateRack:
|
||||||
self.create_child_frame.click_add_button()
|
self.create_child_frame.click_add_button()
|
||||||
|
|
||||||
# Проверяем alert для высоты
|
# Проверяем alert для высоты
|
||||||
self.alert.check_alert_presence(expected_alert_text_height, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_height, timeout=7000)
|
||||||
|
|
||||||
# Проверяем, закрылся ли автоматически alert для высоты
|
# Проверяем, закрылся ли автоматически alert для высоты
|
||||||
self.alert.check_alert_absence(expected_alert_text_height, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text_height, timeout=7000)
|
||||||
|
|
@ -466,7 +466,7 @@ class TestCreateRack:
|
||||||
self.create_child_frame.wait_for_timeout(500)
|
self.create_child_frame.wait_for_timeout(500)
|
||||||
|
|
||||||
# Проверяем alert для имени
|
# Проверяем alert для имени
|
||||||
self.alert.check_alert_presence(expected_alert_text_name, timeout=5000)
|
self.alert.check_alert_presence(expected_alert_text_name, timeout=7000)
|
||||||
|
|
||||||
# Проверяем, закрылся ли автоматически alert для высоты
|
# Проверяем, закрылся ли автоматически alert для высоты
|
||||||
self.alert.check_alert_absence(expected_alert_text_name, timeout=7000)
|
self.alert.check_alert_absence(expected_alert_text_name, timeout=7000)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue