refactor: унификация работы с формами создания и редактирования стоек

- Добавлен базовый класс BaseRackForm с общей логикой для работы с формами
- Вынесена общая функциональность по заполнению полей, очистке и проверке ошибок
- Упрощены классы CreateRackForm и EditRackForm за счет наследования от BaseRackForm
- Обновлены зависимые компоненты (create_element_frame, edit_rack_maker)
- Исправлены тесты создания стойки с учетом новой архитектуры
ra6/create_rack
Radislav 2026-03-11 14:14:15 +03:00
parent 4fff4835f1
commit 0295852986
5 changed files with 212 additions and 1166 deletions

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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,

View File

@ -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)