refactor: глобальная реорганизация структуры проекта

- Добавлена новая архитектура: makers/forms/frames
- Созданы папки:
  - makers/ - сборщики интерфейса
  - forms/ - формы с полями
  - frames/ - обертки и контейнеры
- Удалены устаревшие файлы из components_derived:
  - rack_maker.py (перенесен в makers/accounting_objects/)
  - create_child_element_frame.py (перенесен в frames/)
  - modal_edit_rack.py (заменен на rack_edit_maker.py)
- Удалены неиспользуемые страницы создания элементов
- Добавлен новый тест test_create_rack.py
- Пбновлены существующие тесты test_edit_rack.py и test_management_rack.py
- Исправлены локаторы в rack_locators.py
- Обновлен alert_component.py

Все тесты стойки проходят успешно

Связано: переход на архитектуру Maker/Form/Frame
ra5/create_rack
Radislav 2026-03-06 11:16:19 +03:00
parent 5f518f0aa7
commit da8bde15a1
18 changed files with 2159 additions and 3166 deletions

View File

@ -123,12 +123,13 @@ class AlertComponent(BaseComponent):
).filter(has_text=text)).to_be_hidden(timeout=timeout), msg ).filter(has_text=text)).to_be_hidden(timeout=timeout), msg
logger.info(f"Alert window with text '{text}' successfully disappeared") logger.info(f"Alert window with text '{text}' successfully disappeared")
def check_alert_presence(self, text: str) -> None: def check_alert_presence(self, text: str, timeout: int = 30000) -> None:
"""Проверяет наличие alert-окна с заданным текстом. """Проверяет наличие alert-окна с заданным текстом.
Args: Args:
text: Текст для проверки. Если пустая строка - проверяет только text: Текст для проверки. Если пустая строка - проверяет только
наличие окна. наличие окна.
timeout: Время ожидания появления alert в миллисекундах
Raises: Raises:
AssertionError: Если alert-окно не найдено. AssertionError: Если alert-окно не найдено.
@ -136,12 +137,12 @@ class AlertComponent(BaseComponent):
msg = "Alert window is missing" msg = "Alert window is missing"
if text == "": if text == "":
expect(self.page.get_by_role(AlertLocators.ALERT_ROLE)).to_be_visible(), msg expect(self.page.get_by_role(AlertLocators.ALERT_ROLE)).to_be_visible(timeout=timeout), msg
logger.info(f"Alert window successfully displayed") logger.info(f"Alert window successfully displayed")
else: else:
expect(self.page.get_by_role( expect(self.page.get_by_role(
AlertLocators.ALERT_ROLE AlertLocators.ALERT_ROLE
).filter(has_text=text)).to_be_visible(), msg ).filter(has_text=text)).to_be_visible(timeout=timeout), msg
logger.info(f"Alert window with text '{text}' successfully displayed") logger.info(f"Alert window with text '{text}' successfully displayed")
def check_text(self, alert_text: str) -> None: def check_text(self, alert_text: str) -> None:

View File

@ -1,331 +0,0 @@
"""Модуль создания объекта 'Стойка'."""
from dataclasses import dataclass
from playwright.sync_api import Page, Locator
from tools.logger import get_logger
from locators.rack_locators import RackLocators
from components.base_component import BaseComponent
logger = get_logger("RACK_MAKER")
logger.setLevel("INFO")
@dataclass
class RackData:
"""Класс для хранения данных стойки."""
name: str
height: str = "42"
depth: str = "1000"
serial: str = ""
inventory: str = ""
comment: str = ""
cable_entry: str = ""
state: str = ""
owner: str = ""
service_org: str = ""
project: str = ""
class RackObjectMaker(BaseComponent):
"""Компонент для создания и настройки стойки."""
def __init__(self, page: Page) -> None:
"""
Инициализирует компонент создания стойки.
Args:
page (Page): Экземпляр страницы Playwright
"""
super().__init__(page)
# Действия:
def _fill_combobox_field(self, field_name: str, value: str, fields_locators: dict) -> None:
"""
Заполняет combobox поле.
Args:
field_name (str): Название поля
value (str): Значение для установки
fields_locators (dict): Словарь с найденными полями формы
Raises:
ValueError: Если поле не найдено в форме
"""
# Получаем контейнер поля по его названию
field_container = fields_locators.get(field_name)
if not field_container:
logger.error(f"Field '{field_name}' not found in form. Available fields: {list(fields_locators.keys())}")
raise ValueError(f"Field '{field_name}' not found in form")
logger.debug(f"Filling field '{field_name}' with value '{value}'...")
# Прокручиваем до поля
field_container.scroll_into_view_if_needed()
self.wait_for_timeout(300)
# Проверяем видимость поля
self.check_visibility(field_container, f"Field '{field_name}' not found")
# Находим кнопку открытия выпадающего списка внутри контейнера поля
open_button = field_container.locator(".v-input__append-inner").first
# Кликаем для открытия выпадающего списка
open_button.click(force=True)
self.wait_for_timeout(300)
# Вводим значение из выпадающего списка
dropdown_item_locator = RackLocators.DROPDOWN_ITEM_BY_TEXT.format(value)
element = self.page.locator(dropdown_item_locator).first
# Скроллим к элементу если нужно
self._scroll_until_element(
self.page.locator(RackLocators.DROPDOWN_LIST).first,
value
)
self.wait_for_timeout(300)
element.click()
logger.debug(f"Field '{field_name}' filled successfully")
def _fill_combobox_fields(self, rack_data: RackData) -> None:
"""Заполняет combobox поля."""
# Получаем все поля формы
fields_locators = self._get_form_fields()
# Обязательные поля.
if rack_data.height:
self._fill_combobox_field("Высота в юнитах", rack_data.height, fields_locators)
logger.debug(f"Selected height: {rack_data.height} units")
if rack_data.depth:
self._fill_combobox_field("Глубина (мм)", rack_data.depth, fields_locators)
logger.debug(f"Selected depth: {rack_data.depth} mm")
# Опциональные поля.
if rack_data.cable_entry:
self._fill_combobox_field("Ввод кабеля", rack_data.cable_entry, fields_locators)
logger.debug(f"Selected cable entry: {rack_data.cable_entry}")
if rack_data.state:
self._fill_combobox_field("Состояние", rack_data.state, fields_locators)
logger.debug(f"Selected state: {rack_data.state}")
if rack_data.owner:
self._fill_combobox_field("Владелец", rack_data.owner, fields_locators)
logger.debug(f"Selected owner: {rack_data.owner}")
if rack_data.service_org:
self._fill_combobox_field("Обслуживающая организация", rack_data.service_org, fields_locators)
logger.debug(f"Selected service organization: {rack_data.service_org}")
if rack_data.project:
self._fill_combobox_field("Проект/Титул", rack_data.project, fields_locators)
logger.debug(f"Selected project/title: {rack_data.project}")
def _fill_text_fields(self, rack_data: RackData) -> None:
"""Заполняет текстовые поля."""
logger.debug("Filling text fields...")
# Получаем все поля формы
fields_locators = self._get_form_fields()
logger.debug(f"Available text fields: {list(fields_locators.keys())}")
def clear_and_fill(field_name: str, value: str):
"""Очищает поле и заполняет его значением."""
if not value:
logger.debug(f"Skipping empty value for field '{field_name}'")
return
# Получаем контейнер поля
field_container = fields_locators.get(field_name)
if not field_container:
logger.warning(f"Field '{field_name}' not found in form. Available fields: {list(fields_locators.keys())}")
return
# Находим input внутри контейнера
input_field = field_container.locator("input").first
if input_field.count() == 0:
logger.warning(f"Input element not found in container for field '{field_name}'")
return
# Проверяем видимость
if not input_field.is_visible():
logger.debug(f"Field '{field_name}' is not visible, scrolling into view...")
input_field.scroll_into_view_if_needed()
self.wait_for_timeout(300)
# Проверяем, не disabled ли поле
is_disabled = input_field.get_attribute("disabled")
is_readonly = input_field.get_attribute("readonly")
if is_disabled or is_readonly:
logger.warning(f"Field '{field_name}' is disabled or readonly")
return
# Очищаем поле
input_field.click()
input_field.press("Control+A")
input_field.press("Backspace")
# Заполняем значение
input_field.fill(value)
logger.debug(f"Filled '{field_name}': {value}")
# Обязательные поля
if rack_data.name:
clear_and_fill("Имя", rack_data.name)
# Опциональные поля
if rack_data.serial:
clear_and_fill("Серийный номер", rack_data.serial)
if rack_data.inventory:
clear_and_fill("Инвентарный номер", rack_data.inventory)
if rack_data.comment:
clear_and_fill("Комментарий", rack_data.comment)
logger.debug("Text fields filled successfully")
def _get_form_fields(self) -> dict:
"""
Получает все поля формы стойки.
Returns:
dict: Словарь {название поля: Locator контейнера поля}
Raises:
ValueError: Если контейнер формы не найден
"""
# Получаем контейнер формы (второй элемент)
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1)
if container_locator.count() == 0:
logger.error("Form container not found")
raise ValueError("Form container not found")
return self.get_input_fields_locators(container_locator)
def _scroll_until_element(self, locator: Locator, name: str) -> None:
"""
Скроллит список до тех пор, пока не перестанут подгружаться новые элементы.
Args:
locator (Locator): Локатор элементов или строка с CSS/XPath
name (str): Имя элемента для поиска
"""
loc = self.get_locator(locator)
items_count = 0
attempts = 0
max_attempts = 3
last_item_name = ""
while attempts < max_attempts:
self.page.wait_for_timeout(300)
item_texts = loc.all_inner_texts()
item_names = item_texts[0].splitlines()
current_count = len(item_names)
if current_count == items_count:
attempts += 1
else:
items_count = current_count
attempts = 0
if name in item_names:
last_item_name = name
else:
last_item_name = item_names[current_count-1]
element = loc.get_by_role("listitem").filter(
has_text=last_item_name
)
element.scroll_into_view_if_needed()
self.wait_for_timeout(300)
def fill_rack_data(self, rack_data: RackData) -> None:
"""
Заполняет данные для создания стойки.
Args:
rack_data (RackData): Данные стойки
"""
logger.debug(f"Filling rack data: {rack_data.name}")
self._fill_text_fields(rack_data)
self._fill_combobox_fields(rack_data)
logger.debug("Rack data filled successfully")
# Проверки:
def check_rack_fields_presence(self) -> None:
"""
Проверяет наличие полей специфичных для стойки.
Raises:
AssertionError: Если какое-либо поле не найдено
"""
logger.debug("Checking rack fields presence...")
# Получаем все поля формы
fields_locators = self._get_form_fields()
logger.debug(f"Found fields in form: {list(fields_locators.keys())}")
# Список ожидаемых полей для стойки
expected_fields = [
"Имя",
"Высота в юнитах",
"Глубина (мм)",
"Серийный номер",
"Инвентарный номер",
"Комментарий",
"Ввод кабеля",
"Состояние",
"Владелец",
"Обслуживающая организация",
"Проект/Титул"
]
# Проверяем наличие обязательных полей с помощью assert
required_fields = ["Имя", "Высота в юнитах", "Глубина (мм)"]
for field_name in required_fields:
# Проверяем наличие поля в словаре
assert field_name in fields_locators, f"Required field '{field_name}' not found"
field_container = fields_locators[field_name]
# check_visibility внутри использует expect, который тоже вызывает AssertionError
self.check_visibility(field_container, f"Required field '{field_name}' not visible")
logger.debug(f"Required field '{field_name}' found and visible")
# Проверяем наличие дополнительных полей (только логгирование)
for field_name in expected_fields:
if field_name in fields_locators:
field_container = fields_locators[field_name]
if field_container.is_visible():
logger.debug(f"Optional field '{field_name}' found and visible")
else:
logger.debug(f"Optional field '{field_name}' found but not visible")
else:
logger.debug(f"Optional field '{field_name}' not found in form")
logger.debug("All main rack fields are present")

Binary file not shown.

Binary file not shown.

520
forms/create_rack_form.py Normal file
View File

@ -0,0 +1,520 @@
"""Модуль для работы с формой создания стойки."""
import time
from dataclasses import dataclass
from typing import List, Dict, Any
from playwright.sync_api import Page
from tools.logger import get_logger
from locators.rack_locators import RackLocators
from elements.text_input_element import TextInput
from components.base_component import BaseComponent
from components.dropdown_list_component import DropdownList
logger = get_logger("CREATE_RACK_FORM")
logger.setLevel("INFO")
@dataclass
class CreateRackData:
"""Класс для хранения данных создаваемой стойки."""
# Основные поля
name: str = ""
serial: str = ""
inventory: str = ""
comment: str = ""
# Combobox поля
cable_entry: str = ""
state: str = ""
depth: str = ""
usize: str = ""
# Combobox поля (не заполняемые)
owner: str = ""
service_org: str = ""
project: str = ""
class CreateRackForm(BaseComponent):
"""Компонент для работы с формой создания стойки."""
# Маппинг текстовых полей
TEXT_FIELDS_MAPPING = {
"Имя": ("name", "name_input"),
"Комментарий": ("comment", "comment_input"),
"Серийный номер": ("serial", "serial_input"),
"Инвентарный номер": ("inventory", "inventory_input"),
}
# Маппинг полей для заполнения combobox полей
COMBOBOX_FIELDS_MAPPING = {
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
"Состояние": ("state", "state_input", "state_list"),
"Высота в юнитах": ("usize", "usize_input", "usize_list"),
"Глубина (мм)": ("depth", "depth_input", "depth_list"),
"Владелец": ("owner", "owner_input", "owner_list"),
"Обслуживающая организация": ("service_org", "service_input", "service_list"),
"Проект/Титул": ("project", "project_input", "project_list")
}
# Локаторы для текстовых полей (из RackLocators)
TEXT_FIELDS_LOCATORS = {
"Имя": RackLocators.CREATE_RACK_FORM_FIELD_NAME,
"Комментарий": RackLocators.CREATE_RACK_FORM_FIELD_COMMENT,
"Серийный номер": RackLocators.CREATE_RACK_FORM_FIELD_SERIAL,
"Инвентарный номер": RackLocators.CREATE_RACK_FORM_FIELD_INVENTORY,
}
# Локаторы для combobox полей (из RackLocators)
COMBOBOX_FIELDS_LOCATORS = {
"Высота в юнитах": RackLocators.CREATE_RACK_FORM_SELECT_USIZE,
"Глубина (мм)": RackLocators.CREATE_RACK_FORM_SELECT_DEPTH,
"Ввод кабеля": RackLocators.CREATE_RACK_FORM_SELECT_CABLE_INPUT,
"Состояние": RackLocators.CREATE_RACK_FORM_SELECT_CONDITION_TYPE,
"Владелец": RackLocators.CREATE_RACK_FORM_SELECT_OWNER,
"Обслуживающая организация": RackLocators.CREATE_RACK_FORM_SELECT_SERVICE_PROVIDER,
"Проект/Титул": RackLocators.CREATE_RACK_FORM_SELECT_PROJECT,
}
def __init__(self, page: Page) -> None:
"""Инициализирует компонент формы создания стойки.
Args:
page: Экземпляр страницы Playwright
"""
super().__init__(page)
self.page = page
self.content_items = {}
self.available_fields = None
# Инициализация полей формы
self._init_form_fields()
def _init_form_fields(self) -> None:
"""Инициализирует все поля формы создания."""
# Получаем доступные поля формы
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER).nth(1)
self.available_fields = self.get_input_fields_locators(container_locator)
self._init_text_fields()
self._init_combobox_fields()
def _init_text_fields(self) -> None:
"""Инициализирует текстовые поля формы."""
for field_label, (attr_name, widget_name) in self.TEXT_FIELDS_MAPPING.items():
locator = self.TEXT_FIELDS_LOCATORS.get(field_label)
if not locator:
continue
self._init_single_text_field(field_label, locator, widget_name)
def _init_single_text_field(self, field_label: str, locator: str, widget_name: str) -> None:
"""Инициализирует одно текстовое поле.
Args:
field_label: Метка поля
locator: Локатор поля
widget_name: Имя виджета
"""
try:
element = self.page.locator(locator).first
if element.count() > 0 and element.is_visible():
# Создаем TextInput для поля
field_input = TextInput(self.page, element, widget_name)
self.content_items[widget_name] = field_input
logger.debug(f"Initialized text field: '{field_label}'")
except Exception as e:
logger.error(f"Error initializing text field '{field_label}': {e}")
def _init_combobox_fields(self) -> None:
"""Инициализирует combobox поля формы."""
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label)
if not locator:
continue
self._init_single_combobox_field(field_label, locator, input_name, list_name)
def _init_single_combobox_field(
self, field_label: str, locator: str, input_name: str, list_name: str
) -> None:
"""Инициализирует одно combobox поле.
Args:
field_label: Метка поля
locator: Локатор поля
input_name: Имя поля ввода
list_name: Имя списка
"""
try:
element = self.page.locator(locator).first
if element.count() > 0 and element.is_visible():
# Для combobox создаем TextInput для клика
field_input = TextInput(self.page, element, input_name)
self.content_items[input_name] = field_input
# Добавляем DropdownList для выбора значений
self.content_items[list_name] = DropdownList(self.page)
logger.debug(f"Initialized combobox field: '{field_label}'")
except Exception as e:
logger.error(f"Error initializing combobox field '{field_label}': {e}")
def clear_field(self, field_name: str) -> None:
"""Очищает указанное поле.
Args:
field_name: Название поля для очистки
"""
logger.debug(f"Clearing field: '{field_name}'")
# Получаем локатор поля
locator = None
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
locator = self.COMBOBOX_FIELDS_LOCATORS[field_name]
elif field_name in self.TEXT_FIELDS_LOCATORS:
locator = self.TEXT_FIELDS_LOCATORS[field_name]
else:
logger.warning(f"Unknown field: {field_name}")
return
field_element = self.page.locator(locator).first
if field_element.count() == 0:
logger.debug(f"Field '{field_name}' not found")
return
# Для текстовых полей
if field_name in self.TEXT_FIELDS_LOCATORS:
try:
field_element.click()
field_element.page.keyboard.press("Control+A")
field_element.page.keyboard.press("Backspace")
self.wait_for_timeout(200)
logger.debug(f"Text field '{field_name}' cleared")
except Exception as e:
logger.debug(f"Could not clear text field '{field_name}': {e}")
return
# Для combobox полей
if field_name in self.COMBOBOX_FIELDS_LOCATORS:
# Поднимаемся до родительского контейнера
parent_container = field_element.locator(
"xpath=ancestor::div[contains(@class, 'v-input')]"
).first
if parent_container.count() == 0:
logger.debug(f"Parent container not found for field '{field_name}'")
return
# Ищем кнопку очистки (крестик)
clear_button = parent_container.locator(
".v-input__icon--clear button, .v-input__icon--append button, i.mdi-close-circle, i.mdi-close"
).first
if clear_button.count() > 0 and clear_button.is_visible():
clear_button.click()
self.wait_for_timeout(300)
logger.debug(f"Combobox field '{field_name}' cleared")
else:
logger.debug(f"Clear button not found for field '{field_name}'")
def get_content_item(self, item_name: str) -> Any:
"""Возвращает элемент контента по имени.
Args:
item_name: Имя элемента
Returns:
Элемент или None если не найден
"""
return self.content_items.get(item_name)
def _scroll_to_element_in_dropdown(self, value: str) -> bool:
"""Скроллит выпадающий список до элемента с нужным текстом используя playwright.
Args:
value: Текст для поиска
Returns:
bool: True если элемент найден, False в противном случае
"""
logger.debug(f"Scrolling to find element with text: '{value}'")
# Получаем активное выпадающее меню
dropdown_menu = self.page.locator("div.menuable__content__active").first
if dropdown_menu.count() == 0:
logger.error("Active dropdown menu not found")
return False
max_attempts = 10
attempts = 0
last_item_text = ""
while attempts < max_attempts:
# Получаем все видимые элементы списка
visible_items = dropdown_menu.locator("a.v-list__tile, div[role='listitem']").all()
if visible_items:
# Проверяем каждый видимый элемент
for item in visible_items:
item_text = item.text_content() or ""
if value in item_text:
logger.debug(f"Found element with text '{value}'")
# Скроллим до элемента
item.scroll_into_view_if_needed()
self.wait_for_timeout(300)
return True
# Если элемент не найден, скроллим до последнего видимого элемента
last_item = visible_items[-1]
last_item_text = last_item.text_content() or ""
logger.debug(f"Scrolling to last visible item: '{last_item_text}'")
last_item.scroll_into_view_if_needed()
self.wait_for_timeout(500)
else:
# Если нет видимых элементов, скроллим вниз
dropdown_menu.evaluate("(el) => el.scrollTop += 200")
self.wait_for_timeout(300)
attempts += 1
logger.debug(f"Scroll attempt {attempts}/{max_attempts}")
logger.warning(f"Element with text '{value}' not found after {max_attempts} scroll attempts")
return False
def fill_rack_data(self, rack_data: CreateRackData) -> Dict[str, int]:
"""Заполняет поля формы создания стойки.
Args:
rack_data: Данные для заполнения
Returns:
Словарь с результатами заполнения
"""
results = {
"text_fields_filled": 0,
"combobox_fields_filled": 0,
}
self._fill_text_fields(rack_data, results)
self._fill_combobox_fields(rack_data, results)
logger.info(f"Filled {results['text_fields_filled']} text fields and "
f"{results['combobox_fields_filled']} combobox fields")
return results
def _fill_text_fields(self, rack_data: CreateRackData, results: Dict[str, int]) -> None:
"""Заполняет текстовые поля.
Args:
rack_data: Данные для заполнения
results: Словарь с результатами
"""
for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items():
value = getattr(rack_data, attr_name, "")
if not value or not str(value).strip():
continue
self._fill_single_text_field(field_label, field_name, value, results)
def _fill_single_text_field(
self, field_label: str, field_name: str, value: str, results: Dict[str, int]
) -> None:
"""Заполняет одно текстовое поле.
Args:
field_label: Метка поля
field_name: Имя поля
value: Значение для заполнения
results: Словарь с результатами
"""
try:
input_field = self.get_content_item(field_name)
if input_field:
input_field.input_value(value)
results["text_fields_filled"] += 1
logger.debug(f"Field '{field_label}' filled: '{value}'")
except Exception as e:
logger.error(f"Error filling field '{field_label}': {e}")
def _fill_combobox_fields(self, rack_data: CreateRackData, results: Dict[str, int]) -> None:
"""Заполняет combobox поля.
Args:
rack_data: Данные для заполнения
results: Словарь с результатами
"""
for field_label, (attr_name, input_name, list_name) in self.COMBOBOX_FIELDS_MAPPING.items():
value = getattr(rack_data, attr_name, "")
if not value or not str(value).strip():
continue
self._fill_single_combobox_field(
field_label, input_name, list_name, value, results
)
def _fill_single_combobox_field(
self, field_label: str, input_name: str, list_name: str, value: str, results: Dict[str, int]
) -> None:
"""Заполняет одно combobox поле.
Args:
field_label: Метка поля
input_name: Имя поля ввода
list_name: Имя списка
value: Значение для выбора
results: Словарь с результатами
"""
try:
combobox_field = self.get_content_item(input_name)
if not combobox_field:
logger.warning(f"Field '{field_label}' input not found")
return
# Кликаем для открытия выпадающего списка
combobox_field.click(force=True)
self.wait_for_timeout(1000)
# Скроллим до нужного элемента
if not self._scroll_to_element_in_dropdown(value):
logger.error(f"Could not find element with text '{value}' after scrolling")
# Закрываем выпадающий список кликом вне
self.page.mouse.click(10, 10)
self.wait_for_timeout(300)
return
# Получаем активное выпадающее меню
dropdown_menu = self.page.locator("div.menuable__content__active").first
# Ищем элемент с нужным текстом
item_locator = dropdown_menu.locator(f"a.v-list__tile:has-text('{value}')").first
if item_locator.count() == 0:
item_locator = dropdown_menu.locator(f"span:has-text('{value}')").first
if item_locator.count() == 0:
item_locator = dropdown_menu.locator(f"div[role='listitem']:has-text('{value}')").first
if item_locator.count() > 0:
# Убеждаемся что элемент видим и кликаем
item_locator.scroll_into_view_if_needed()
self.wait_for_timeout(300)
item_locator.click()
results["combobox_fields_filled"] += 1
logger.debug(f"Field '{field_label}' set: '{value}'")
# Небольшая пауза после выбора
self.wait_for_timeout(500)
else:
logger.error(f"Item with text '{value}' not found in dropdown for field '{field_label}'")
# Закрываем выпадающий список кликом вне
self.page.mouse.click(10, 10)
self.wait_for_timeout(300)
except Exception as e:
logger.error(f"Error filling combobox '{field_label}': {e}")
self.page.mouse.click(10, 10)
def is_field_highlighted_as_error(self, field_name: str) -> bool:
"""Проверяет, подсвечено ли поле как ошибочное.
Args:
field_name: Название поля для проверки
Returns:
bool: True если поле подсвечено ошибкой, False в противном случае
"""
# Проверяем в текстовых полях
if field_name in self.TEXT_FIELDS_LOCATORS:
locator = self.TEXT_FIELDS_LOCATORS[field_name]
field_element = self.page.locator(locator).first
if field_element.count() == 0:
logger.debug(f"Field '{field_name}' not found")
return False
# Поднимаемся до родительского контейнера с классом v-input
parent_input = field_element.locator("xpath=ancestor::div[contains(@class, 'v-input')]").first
if parent_input.count() > 0:
# Проверяем наличие класса ошибки
class_attr = parent_input.get_attribute("class") or ""
is_error = "v-input--error" in class_attr or "error--text" in class_attr
logger.debug(f"Field '{field_name}' error state: {is_error}, classes: {class_attr}")
return is_error
# Проверяем в combobox полях
elif field_name in self.COMBOBOX_FIELDS_LOCATORS:
locator = self.COMBOBOX_FIELDS_LOCATORS[field_name]
field_element = self.page.locator(locator).first
if field_element.count() == 0:
logger.debug(f"Field '{field_name}' not found")
return False
# Поднимаемся до родительского контейнера с классом v-input
parent_input = field_element.locator("xpath=ancestor::div[contains(@class, 'v-input')]").first
if parent_input.count() > 0:
# Проверяем наличие класса ошибки
class_attr = parent_input.get_attribute("class") or ""
is_error = "v-input--error" in class_attr or "error--text" in class_attr
logger.debug(f"Field '{field_name}' error state: {is_error}, classes: {class_attr}")
return is_error
return False
def verify_required_fields_highlighted(self, field_names: List[str]) -> Dict[str, bool]:
"""Проверяет, что указанные поля подсвечены как обязательные (с ошибкой).
Args:
field_names: Список названий полей для проверки
Returns:
Словарь с результатами проверки {field_name: is_highlighted}
"""
results = {}
for field_name in field_names:
results[field_name] = self.is_field_highlighted_as_error(field_name)
logger.debug(f"Field '{field_name}' highlighted: {results[field_name]}")
return results
def wait_for_field_error(self, field_name: str, timeout: int = 5000) -> bool:
"""Ожидает появления подсветки ошибки на поле.
Args:
field_name: Название поля
timeout: Таймаут в миллисекундах
Returns:
bool: True если ошибка появилась, False в противном случае
"""
start_time = time.time()
while (time.time() - start_time) * 1000 < timeout:
if self.is_field_highlighted_as_error(field_name):
return True
self.wait_for_timeout(200)
return False

649
forms/edit_rack_form.py Normal file
View File

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

View File

@ -11,11 +11,11 @@ from components.toolbar_component import ToolbarComponent
from components_derived.selection_bar_component import SelectionBarComponent from components_derived.selection_bar_component import SelectionBarComponent
logger = get_logger("CREATE_CHILD_ELEMENT_FRAME") logger = get_logger("CREATE_ELEMENT_FRAME")
logger.setLevel("INFO") logger.setLevel("INFO")
class CreateChildElementFrame(BaseComponent):
class CreateElementFrame(BaseComponent):
"""Фрейм создания дочернего элемента.""" """Фрейм создания дочернего элемента."""
def __init__(self, page: Page) -> None: def __init__(self, page: Page) -> None:
@ -25,7 +25,6 @@ class CreateChildElementFrame(BaseComponent):
Args: Args:
page (Page): Экземпляр страницы Playwright page (Page): Экземпляр страницы Playwright
""" """
super().__init__(page) super().__init__(page)
# Инициализация компонентов # Инициализация компонентов
@ -38,7 +37,7 @@ class CreateChildElementFrame(BaseComponent):
has_text="Создать дочерний элемент в" has_text="Создать дочерний элемент в"
).get_by_role("button").nth(0) ).get_by_role("button").nth(0)
# Кнопка "Отменить" - используем рабочий локатор из старой версии # Кнопка "Отменить" - используем рабочий локатор
cancel_button_locator = self.page.get_by_role("navigation").filter( cancel_button_locator = self.page.get_by_role("navigation").filter(
has_text=re.compile('Создать дочерний элемент в') has_text=re.compile('Создать дочерний элемент в')
).get_by_role("button").nth(1) ).get_by_role("button").nth(1)
@ -56,25 +55,20 @@ class CreateChildElementFrame(BaseComponent):
Args: Args:
field_name (str): Название поля для очистки field_name (str): Название поля для очистки
""" """
logger.debug(f"Clearing combobox field '{field_name}'...") logger.debug(f"Clearing combobox field '{field_name}'...")
# Получаем контейнер формы # Получаем контейнер формы
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1) container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER).nth(1)
fields_locators = self.get_input_fields_locators(container_locator) fields_locators = self.get_input_fields_locators(container_locator)
if field_name not in fields_locators: if field_name not in fields_locators:
logger.warning(f"Field '{field_name}' not found in form") logger.warning(f"Field '{field_name}' not found in form")
return return
# Получаем контейнер поля
field_container = fields_locators[field_name] field_container = fields_locators[field_name]
# Прокручиваем до поля
field_container.scroll_into_view_if_needed() field_container.scroll_into_view_if_needed()
self.wait_for_timeout(300) self.wait_for_timeout(300)
# Проверяем видимость
if not field_container.is_visible(): if not field_container.is_visible():
logger.debug(f"Field '{field_name}' is not visible after scrolling") logger.debug(f"Field '{field_name}' is not visible after scrolling")
return return
@ -82,11 +76,7 @@ class CreateChildElementFrame(BaseComponent):
# Ищем кнопку закрытия (крестик) внутри контейнера поля # Ищем кнопку закрытия (крестик) внутри контейнера поля
close_button = field_container.locator("i.mdi-close").first close_button = field_container.locator("i.mdi-close").first
# Проверяем наличие и видимость кнопки закрытия
if close_button.count() > 0: if close_button.count() > 0:
logger.debug(f"Found close button for field '{field_name}'")
# Если кнопка закрытия видима - кликаем на нее
close_button.click(force=True) close_button.click(force=True)
self.wait_for_timeout(300) self.wait_for_timeout(300)
logger.debug(f"Combobox field '{field_name}' cleared using close button") logger.debug(f"Combobox field '{field_name}' cleared using close button")
@ -95,13 +85,11 @@ class CreateChildElementFrame(BaseComponent):
def click_add_button(self) -> None: def click_add_button(self) -> None:
"""Кликает на кнопку 'Добавить'.""" """Кликает на кнопку 'Добавить'."""
logger.debug("Clicking on 'Add' button...") logger.debug("Clicking on 'Add' button...")
self.toolbar.click_button("add") self.toolbar.click_button("add")
def click_cancel_button(self) -> None: def click_cancel_button(self) -> None:
"""Кликает на кнопку 'Отменить'.""" """Кликает на кнопку 'Отменить'."""
logger.debug("Clicking on 'Cancel' button...") logger.debug("Clicking on 'Cancel' button...")
self.toolbar.click_button("cancel") self.toolbar.click_button("cancel")
@ -112,7 +100,6 @@ class CreateChildElementFrame(BaseComponent):
Returns: Returns:
str: Выбранный класс объекта или пустая строка если ничего не выбрано str: Выбранный класс объекта или пустая строка если ничего не выбрано
""" """
return self.selection_bar.get_selection_bar_title() return self.selection_bar.get_selection_bar_title()
def is_field_filled(self, field_name: str, container_locator: Locator = None) -> bool: def is_field_filled(self, field_name: str, container_locator: Locator = None) -> bool:
@ -126,38 +113,28 @@ class CreateChildElementFrame(BaseComponent):
Returns: Returns:
bool: True если поле заполнено, False в противном случае bool: True если поле заполнено, False в противном случае
""" """
logger.debug(f"Checking if field '{field_name}' is filled...") logger.debug(f"Checking if field '{field_name}' is filled...")
# Если контейнер не передан, используем контейнер по умолчанию
if container_locator is None: if container_locator is None:
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1) container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER).nth(1)
# Получаем словарь всех полей формы
fields_locators = self.get_input_fields_locators(container_locator) fields_locators = self.get_input_fields_locators(container_locator)
if field_name not in fields_locators: if field_name not in fields_locators:
logger.debug(f"Field '{field_name}' not found in fields_locators") logger.debug(f"Field '{field_name}' not found in fields_locators")
return False return False
# Получаем контейнер поля
field_container = fields_locators[field_name] field_container = fields_locators[field_name]
if not field_container.is_visible(): if not field_container.is_visible():
logger.debug(f"Field '{field_name}' not visible") logger.debug(f"Field '{field_name}' not visible")
return False return False
# Проверяем наличие выбранного значения через v-chip (чип выбранного значения в combobox)
selected_chip = field_container.locator(".v-chip").first selected_chip = field_container.locator(".v-chip").first
# Проверяем наличие текста в поле
field_text = field_container.text_content() or "" field_text = field_container.text_content() or ""
has_text = bool(field_text.strip()) has_text = bool(field_text.strip())
# Проверяем наличие чипа
has_chip = selected_chip.count() > 0 and selected_chip.is_visible() has_chip = selected_chip.count() > 0 and selected_chip.is_visible()
# Для текстовых полей проверяем значение input
if not has_chip: if not has_chip:
input_field = field_container.locator("input").first input_field = field_container.locator("input").first
if input_field.count() > 0: if input_field.count() > 0:
@ -167,13 +144,11 @@ class CreateChildElementFrame(BaseComponent):
has_text = has_text or has_input_value has_text = has_text or has_input_value
logger.debug(f"Field '{field_name}' - has chip: {has_chip}, has text: {has_text}") logger.debug(f"Field '{field_name}' - has chip: {has_chip}, has text: {has_text}")
return has_chip or has_text return has_chip or has_text
def open_object_class_combobox(self) -> None: def open_object_class_combobox(self) -> None:
"""Открывает выпадающий список combobox.""" """Открывает выпадающий список combobox."""
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER)
fields_locators = self.get_input_fields_locators(container_locator) fields_locators = self.get_input_fields_locators(container_locator)
combobox_container = fields_locators.get("Класс объекта учета") combobox_container = fields_locators.get("Класс объекта учета")
@ -181,12 +156,10 @@ class CreateChildElementFrame(BaseComponent):
logger.error("Combobox 'Класс объекта учета' not found") logger.error("Combobox 'Класс объекта учета' not found")
return return
# Проверяем, не открыт ли уже выпадающий список
menu_selector = "div.v-menu__content.menuable__content__active" menu_selector = "div.v-menu__content.menuable__content__active"
is_menu_open = self.page.locator(menu_selector).count() > 0 is_menu_open = self.page.locator(menu_selector).count() > 0
if not is_menu_open: if not is_menu_open:
# Используем OPEN_PARAMETERS_LIST_BUTTON из SelectionBarLocators
open_button = combobox_container.locator(SelectionBarLocators.OPEN_PARAMETERS_LIST_BUTTON) open_button = combobox_container.locator(SelectionBarLocators.OPEN_PARAMETERS_LIST_BUTTON)
open_button.click(force=True, timeout=5000) open_button.click(force=True, timeout=5000)
else: else:
@ -199,18 +172,10 @@ class CreateChildElementFrame(BaseComponent):
Args: Args:
class_name (str): Название класса объекта для выбора class_name (str): Название класса объекта для выбора
""" """
logger.debug(f"Selecting object class: '{class_name}'...") logger.debug(f"Selecting object class: '{class_name}'...")
# Открываем combobox
self.open_object_class_combobox() self.open_object_class_combobox()
# Выбирает значение из списка
self.selection_bar.select_value(class_name) self.selection_bar.select_value(class_name)
# Даем время на применение выбора
self.wait_for_timeout(300) self.wait_for_timeout(300)
logger.debug(f"Object class '{class_name}' successfully selected") logger.debug(f"Object class '{class_name}' successfully selected")
# Проверки: # Проверки:
@ -226,23 +191,16 @@ class CreateChildElementFrame(BaseComponent):
ValueError: Если поле не найдено в форме ValueError: Если поле не найдено в форме
AssertionError: Если поле не подсвечено ошибкой AssertionError: Если поле не подсвечено ошибкой
""" """
logger.debug(f"Checking field '{field_name}' for error highlighting...") logger.debug(f"Checking field '{field_name}' for error highlighting...")
# Получаем контейнеры всех полей container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER)
fields_locators = self.get_input_fields_locators(container_locator) fields_locators = self.get_input_fields_locators(container_locator)
# Получаем контейнер конкретного поля
field_container = fields_locators.get(field_name) field_container = fields_locators.get(field_name)
if not field_container: if not field_container:
raise ValueError(f"Field '{field_name}' not found in form") raise ValueError(f"Field '{field_name}' not found in form")
# Ищем элементы с классами ошибки внутри контейнера поля
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS) error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
# Проверяем, что есть хотя бы один элемент с классом ошибки
has_error = error_elements.count() > 0 has_error = error_elements.count() > 0
assert has_error, ( assert has_error, (
@ -263,23 +221,16 @@ class CreateChildElementFrame(BaseComponent):
ValueError: Если поле не найдено в форме ValueError: Если поле не найдено в форме
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...")
# Получаем контейнеры всех полей container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER)
fields_locators = self.get_input_fields_locators(container_locator) fields_locators = self.get_input_fields_locators(container_locator)
# Получаем контейнер конкретного поля
field_container = fields_locators.get(field_name) field_container = fields_locators.get(field_name)
if not field_container: if not field_container:
raise ValueError(f"Field '{field_name}' not found in form") raise ValueError(f"Field '{field_name}' not found in form")
# Ищем элементы с классами ошибки внутри контейнера поля
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS) error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
# Проверяем, что нет элементов с классами ошибки
has_error = error_elements.count() > 0 has_error = error_elements.count() > 0
assert not has_error, ( assert not has_error, (
@ -299,7 +250,6 @@ class CreateChildElementFrame(BaseComponent):
Raises: Raises:
AssertionError: Если выбранный класс не соответствует ожидаемому AssertionError: Если выбранный класс не соответствует ожидаемому
""" """
logger.debug(f"Checking selected object class: '{expected_class}'...") logger.debug(f"Checking selected object class: '{expected_class}'...")
self.wait_for_timeout(500) self.wait_for_timeout(500)
@ -313,10 +263,7 @@ class CreateChildElementFrame(BaseComponent):
f"Expected: '{expected_class}', Got: '{actual_class}'" f"Expected: '{expected_class}', Got: '{actual_class}'"
) )
logger.debug( logger.debug(f"Object class '{expected_class}' successfully selected (actual: '{actual_class}')")
f"Object class '{expected_class}' successfully selected "
f"(actual: '{actual_class}')"
)
def check_toolbar_title(self, expected_title: str) -> None: def check_toolbar_title(self, expected_title: str) -> None:
""" """
@ -328,17 +275,14 @@ class CreateChildElementFrame(BaseComponent):
Raises: Raises:
AssertionError: Если заголовок не соответствует ожидаемому AssertionError: Если заголовок не соответствует ожидаемому
""" """
logger.debug(f"Checking toolbar title: '{expected_title}'...") logger.debug(f"Checking toolbar title: '{expected_title}'...")
# Используем метод тулбара с фильтрацией по тексту
actual_text = self.toolbar.get_toolbar_title_text( actual_text = self.toolbar.get_toolbar_title_text(
filter_text="Создать дочерний элемент в" filter_text="Создать дочерний элемент в"
) )
assert expected_title in actual_text, ( assert expected_title in actual_text, (
f"Title does not match. Expected: '{expected_title}', " f"Title does not match. Expected: '{expected_title}', Got: '{actual_text}'"
f"Got: '{actual_text}'"
) )
logger.debug(f"Toolbar title is correct: '{actual_text}'") logger.debug(f"Toolbar title is correct: '{actual_text}'")
@ -347,7 +291,6 @@ class CreateChildElementFrame(BaseComponent):
""" """
Проверяет наличие и функциональность кнопок тулбара. Проверяет наличие и функциональность кнопок тулбара.
""" """
self.toolbar.check_button_visibility("add") self.toolbar.check_button_visibility("add")
self.toolbar.check_button_tooltip("add", "Добавить") self.toolbar.check_button_tooltip("add", "Добавить")
self.toolbar.check_button_visibility("cancel") self.toolbar.check_button_visibility("cancel")

View File

@ -28,33 +28,54 @@ class RackLocators:
ACTIVE_TAB = ("//div[@data-testid='CABINET_SHOW__tabs']" ACTIVE_TAB = ("//div[@data-testid='CABINET_SHOW__tabs']"
"//a[contains(@class, 'v-tabs__item--active')]") "//a[contains(@class, 'v-tabs__item--active')]")
# Контейнер формы создания/редактирования стойки # ================ ЛОКАТОРЫ ДЛЯ ФОРМЫ СОЗДАНИЯ СТОЙКИ ===================
FORM_INPUT_CONTAINER = "//div[contains(@class, 'flex xs6 pa-0')]"
# Контейнер формы создания стойки
CREATE_RACK_FORM_CONTAINER = "//div[contains(@class, 'flex xs6 pa-0')]"
# Text
CREATE_RACK_FORM_FIELD_NAME = "[data-testid='create-location-bar__text-field__name']"
CREATE_RACK_FORM_FIELD_COMMENT = "[data-testid='create-location-bar__text-field__comment']"
CREATE_RACK_FORM_FIELD_SERIAL = "[data-testid='create-location-bar__text-field__serial_number']"
CREATE_RACK_FORM_FIELD_INVENTORY = "[data-testid='create-location-bar__text-field__inventory_number']"
# Сombobox
CREATE_RACK_FORM_SELECT_USIZE = "[data-testid='create-location-bar__select__usize']"
CREATE_RACK_FORM_SELECT_DEPTH = "[data-testid='create-location-bar__select__depth']"
CREATE_RACK_FORM_SELECT_CABLE_INPUT = "[data-testid='create-location-bar__select__cable_input']"
CREATE_RACK_FORM_SELECT_CONDITION_TYPE = "[data-testid='create-location-bar__select__condition_type']"
CREATE_RACK_FORM_SELECT_OWNER = "[data-testid='create-location-bar__select__owner']"
CREATE_RACK_FORM_SELECT_SERVICE_PROVIDER = "[data-testid='create-location-bar__select__service_provider']"
CREATE_RACK_FORM_SELECT_PROJECT = "[data-testid='create-location-bar__select__project']"
# ================ ЛОКАТОРЫ ДЛЯ ФОРМЫ РЕДАКТИРОВАНИЯ СТОЙКИ ===================
# Форма редактирования стойки в модальном окне # Форма редактирования стойки в модальном окне
RACK_EDIT_FORM = "[data-testid='cabinet-bar__cabinet-form']" EDIT_RACK_FORM = "[data-testid='cabinet-bar__cabinet-form']"
# Локаторы полей формы # Text
INPUT_FORM_RACK_DATA = f"{RACK_EDIT_FORM}" EDIT_RACK_FORM_FIELD_NAME = "[data-testid='cabinet-bar__main__text-field__name']"
INPUT_FORM_RACK_DATA_FIELD_NAME = "[data-testid='cabinet-bar__main__text-field__name']" EDIT_RACK_FORM_FIELD_COMMENT = "[data-testid='cabinet-bar__main__text-field__comment']"
INPUT_FORM_RACK_DATA_FIELD_COMMENT = "[data-testid='cabinet-bar__main__text-field__comment']" EDIT_RACK_FORM_FIELD_SERIAL = "[data-testid='cabinet-bar__main__text-field__serial_number']"
INPUT_FORM_RACK_DATA_FIELD_SERIAL = "[data-testid='cabinet-bar__main__text-field__serial_number']" EDIT_RACK_FORM_FIELD_INVENTORY = "[data-testid='cabinet-bar__main__text-field__inventory_number']"
INPUT_FORM_RACK_DATA_FIELD_INVENTORY = "[data-testid='cabinet-bar__main__text-field__inventory_number']" EDIT_RACK_FORM_FIELD_POWER = "[data-testid='cabinet-bar__main__text-field__allocated_power']"
INPUT_FORM_RACK_DATA_FIELD_POWER = "[data-testid='cabinet-bar__main__text-field__allocated_power']"
# Локаторы для combobox полей # Сombobox
INPUT_FORM_RACK_DATA_FIELD_CABLE_ENTRY = "[data-testid='cabinet-bar__select_enum__select-field__cable_input']" EDIT_RACK_FORM_SELECT_CABLE_INPUT = "[data-testid='cabinet-bar__select_enum__select-field__cable_input']"
INPUT_FORM_RACK_DATA_FIELD_CONDITION_TYPE = "[data-testid='cabinet-bar__select_enum__select-field__condition_type']" EDIT_RACK_FORM_SELECT_CONDITION_TYPE = "[data-testid='cabinet-bar__select_enum__select-field__condition_type']"
INPUT_FORM_RACK_DATA_FIELD_DEPTH = "[data-testid='cabinet-bar__select_enum__select-field__depth']" EDIT_RACK_FORM_SELECT_DEPTH = "[data-testid='cabinet-bar__select_enum__select-field__depth']"
INPUT_FORM_RACK_DATA_FIELD_USIZE = "[data-testid='cabinet-bar__select_enum__select-field__usize']" EDIT_RACK_FORM_SELECT_USIZE = "[data-testid='cabinet-bar__select_enum__select-field__usize']"
INPUT_FORM_RACK_DATA_FIELD_OWNER = "[data-testid='cabinet-bar__select__select-field__owner']" EDIT_RACK_FORM_SELECT_OWNER = "[data-testid='cabinet-bar__select__select-field__owner']"
INPUT_FORM_RACK_DATA_FIELD_SERVICE_PROVIDER = "[data-testid='cabinet-bar__select__select-field__service_provider']" EDIT_RACK_FORM_SELECT_SERVICE_PROVIDER = "[data-testid='cabinet-bar__select__select-field__service_provider']"
INPUT_FORM_RACK_DATA_FIELD_PROJECT = "[data-testid='cabinet-bar__select__select-field__project']" EDIT_RACK_FORM_SELECT_PROJECT = "[data-testid='cabinet-bar__select__select-field__project']"
# Чекбоксы # Checkbox
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel'] input[type='checkbox']" EDIT_RACK_FORM_CHECKBOX_VENTILATION = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel'] input[type='checkbox']"
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_LABEL = "label:has-text('Вентиляционная панель')" EDIT_RACK_FORM_CHECKBOX_VENTILATION_LABEL = "label:has-text('Вентиляционная панель')"
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_CONTAINER = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel']" EDIT_RACK_FORM_DATA_CHECKBOX_VENTILATION_CONTAINER = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel']"
# ================ ЛОКАТОРЫ ДЛЯ ВЫПАДАЮЩИХ СПИСКОВ ===================
# Локаторы для меню combobox # Локаторы для меню combobox
MENU_ACTIVE_RACK_FORM = "//div[contains(@class, 'menuable__content__active')]" MENU_ACTIVE_RACK_FORM = "//div[contains(@class, 'menuable__content__active')]"

Binary file not shown.

View File

@ -1,63 +1,32 @@
"""Модуль для работы с модальным окном редактирования стойки.""" """Модуль для работы с модальным окном редактирования стойки."""
import re import re
from dataclasses import dataclass
from typing import Optional, List, Tuple, Any from typing import Optional, List, Tuple, Any
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 elements.text_element import Text
from elements.checkbox_element import Checkbox
from components.modal_window_component import ModalWindowComponent 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_element import Text
from forms.edit_rack_form import EditRackForm, EditRackFormData
logger = get_logger("MODAL_EDIT_RACK") logger = get_logger("EDIT_RACK_MAKER")
logger.setLevel("INFO") logger.setLevel("INFO")
@dataclass
class RackEditData:
"""Класс для хранения данных редактирования стойки.
Содержит все возможные поля, которые могут быть изменены # Используем EditRackFormData
в модальном окне редактирования стойки. EditRackData = EditRackFormData
"""
# Основные поля (редактируемые)
name: str = ""
serial: str = ""
inventory: str = ""
comment: str = ""
allocated_power: str = ""
# Combobox поля (редактируемые)
cable_entry: str = ""
state: str = ""
depth: str = ""
usize: str = ""
owner: str = ""
service_org: str = ""
project: str = ""
# Checkbox поля (редактируемые)
ventilation_panel: Optional[bool] = None
# Правила доступа
read_access_rules: str = ""
write_access_rules: str = ""
sms_access_rules: str = ""
email_access_rules: str = ""
push_access_rules: str = ""
class ModalEditRack(ModalWindowComponent): class EditRackMaker(ModalWindowComponent):
"""Компонент для работы с модальным окном редактирования стойки. """Компонент для работы с модальным окном редактирования стойки.
Предоставляет методы для взаимодействия с элементами окна: Предоставляет методы для взаимодействия с элементами окна:
- переключение между вкладками - переключение между вкладками
- заполнение полей общей информации - заполнение полей общей информации (через EditRackForm)
- работа с изображениями - работа с изображениями
- настройка правил доступа - настройка правил доступа
- сохранение/отмена изменений - сохранение/отмена изменений
@ -69,47 +38,7 @@ class ModalEditRack(ModalWindowComponent):
TAB_IMAGE = "Изображение" TAB_IMAGE = "Изображение"
TAB_SETTINGS = "Настройки" TAB_SETTINGS = "Настройки"
# Маппинг полей для заполнения текстовых полей # Маппинг полей для вкладки "Настройки" - оставляем только то, что специфично для модального окна
TEXT_FIELDS_MAPPING = {
"Имя": ("name", "name_input"),
"Комментарий": ("comment", "comment_input"),
"Серийный номер": ("serial", "serial_input"),
"Инвентарный номер": ("inventory", "inventory_input"),
"Выделенная мощность (Вт/ВА)": ("allocated_power", "power_input"),
}
# Маппинг полей для заполнения combobox полей
COMBOBOX_FIELDS_MAPPING = {
"Ввод кабеля": ("cable_entry", "cable_entry_input", "cable_entry_list"),
"Состояние": ("state", "state_input", "state_list"),
"Глубина (мм)": ("depth", "depth_input", "depth_list"),
"Высота в юнитах": ("usize", "usize_input", "usize_list"),
"Владелец": ("owner", "owner_input", "owner_list"),
"Обслуживающая организация": ("service_org", "service_input", "service_list"),
"Проект/Титул": ("project", "project_input", "project_list")
}
# Локаторы для текстовых полей (из RackLocators)
TEXT_FIELDS_LOCATORS = {
"Имя": RackLocators.INPUT_FORM_RACK_DATA_FIELD_NAME,
"Комментарий": RackLocators.INPUT_FORM_RACK_DATA_FIELD_COMMENT,
"Серийный номер": RackLocators.INPUT_FORM_RACK_DATA_FIELD_SERIAL,
"Инвентарный номер": RackLocators.INPUT_FORM_RACK_DATA_FIELD_INVENTORY,
"Выделенная мощность (Вт/ВА)": RackLocators.INPUT_FORM_RACK_DATA_FIELD_POWER,
}
# Локаторы для combobox полей (из RackLocators)
COMBOBOX_FIELDS_LOCATORS = {
"Ввод кабеля": RackLocators.INPUT_FORM_RACK_DATA_FIELD_CABLE_ENTRY,
"Состояние": RackLocators.INPUT_FORM_RACK_DATA_FIELD_CONDITION_TYPE,
"Глубина (мм)": RackLocators.INPUT_FORM_RACK_DATA_FIELD_DEPTH,
"Высота в юнитах": RackLocators.INPUT_FORM_RACK_DATA_FIELD_USIZE,
"Владелец": RackLocators.INPUT_FORM_RACK_DATA_FIELD_OWNER,
"Обслуживающая организация": RackLocators.INPUT_FORM_RACK_DATA_FIELD_SERVICE_PROVIDER,
"Проект/Титул": RackLocators.INPUT_FORM_RACK_DATA_FIELD_PROJECT,
}
# Маппинг полей для вкладки "Настройки"
ACCESS_RULES_MAPPING = { ACCESS_RULES_MAPPING = {
"Правила доступа для чтения": ( "Правила доступа для чтения": (
"read_access_rules", "rules_read_input", "rules_read_list" "read_access_rules", "rules_read_input", "rules_read_list"
@ -148,11 +77,11 @@ class ModalEditRack(ModalWindowComponent):
super().__init__(page) super().__init__(page)
self.rack_name = rack_name self.rack_name = rack_name
self.page = page self.page = page
self.available_fields = None
self.active_tab = self.TAB_GENERAL self.active_tab = self.TAB_GENERAL
self.tabs = {} self.tabs = {}
self.content_items = {} self.content_items = {}
self.delete_confirm = None self.delete_confirm = None
self.edit_form = None
# Настройка заголовка и кнопки закрытия # Настройка заголовка и кнопки закрытия
self.window_title = rack_name self.window_title = rack_name
@ -198,101 +127,11 @@ class ModalEditRack(ModalWindowComponent):
def _init_general_tab_content(self) -> None: def _init_general_tab_content(self) -> None:
"""Инициализирует содержимое вкладки 'Общая информация'.""" """Инициализирует содержимое вкладки 'Общая информация'."""
# Получаем доступные поля формы с помощью базового метода # Инициализируем форму редактирования
self.available_fields = self.get_input_fields_locators( self.edit_form = EditRackForm(self.page)
self.page.locator(RackLocators.INPUT_FORM_RACK_DATA)) # Копируем content_items из формы
self.content_items.update(self.edit_form.content_items)
self._init_text_fields() logger.debug("General tab content initialized via EditRackForm")
self._init_combobox_fields()
self._init_checkbox_fields()
def _init_text_fields(self) -> None:
"""Инициализирует текстовые поля формы."""
for field_label, (_, 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():
field_input = TextInput(self.page, element, widget_name)
self.add_content_item(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, (_, 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():
field_input = TextInput(self.page, element, input_name)
self.add_content_item(input_name, field_input)
self.add_content_item(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(
RackLocators.INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION
).first
if checkbox_input.count() == 0:
return
checkbox = Checkbox(self.page, checkbox_input, "ventilation_panel")
self.add_content_item("ventilation_checkbox", checkbox)
label_locator = self.page.locator("label:has-text('Вентиляционная панель')").first
if label_locator.count() > 0:
label_text = Text(self.page, label_locator, "ventilation_checkbox_label")
self.add_content_item("ventilation_checkbox_label", label_text)
logger.debug("Initialized ventilation panel checkbox")
def _init_image_tab_content(self) -> None: def _init_image_tab_content(self) -> None:
"""Инициализирует содержимое вкладки 'Изображение'.""" """Инициализирует содержимое вкладки 'Изображение'."""
@ -1020,7 +859,7 @@ class ModalEditRack(ModalWindowComponent):
save_button.click() save_button.click()
logger.debug("Clicked done button") logger.debug("Clicked done button")
def fill_rack_data(self, rack_data: RackEditData) -> dict: def fill_rack_data(self, rack_data: EditRackData) -> dict:
"""Заполняет поля формы редактирования стойки. """Заполняет поля формы редактирования стойки.
Args: Args:
@ -1033,139 +872,23 @@ class ModalEditRack(ModalWindowComponent):
if self.active_tab != self.TAB_GENERAL: if self.active_tab != self.TAB_GENERAL:
self.switch_to_tab(self.TAB_GENERAL) self.switch_to_tab(self.TAB_GENERAL)
results = { # Используем форму для заполнения данных
"text_fields_filled": 0, if self.edit_form:
"combobox_fields_filled": 0, results = self.edit_form.fill_rack_data(rack_data)
"checkboxes_set": 0 logger.info(f"Filled rack data via EditRackForm: {results}")
} return results
else:
self._fill_text_fields(rack_data, results) logger.error("Edit form not initialized")
self._fill_combobox_fields(rack_data, results) return {
self._set_checkbox(rack_data, results) "text_fields_filled": 0,
"combobox_fields_filled": 0,
return results "checkboxes_set": 0
}
def _fill_text_fields(self, rack_data: RackEditData, results: dict) -> 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
) -> 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.info(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: RackEditData, results: dict) -> 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
) -> None:
"""Заполняет одно combobox поле.
Args:
field_label: Метка поля.
input_name: Имя поля ввода.
list_name: Имя списка.
value: Значение для выбора.
results: Словарь с результатами.
"""
try:
combobox_field = self.get_content_item(input_name)
if not combobox_field:
return
combobox_field.click(force=True)
self.wait_for_timeout(500)
dropdown_list = self.get_content_item(list_name)
if dropdown_list:
dropdown_list.click_item_with_text(value)
results["combobox_fields_filled"] += 1
logger.info(f"Field '{field_label}' set: '{value}'")
except Exception as e:
logger.error(f"Error filling combobox '{field_label}': {e}")
def _set_checkbox(self, rack_data: RackEditData, results: dict) -> None:
"""Устанавливает чекбокс.
Args:
rack_data: Данные для заполнения.
results: Словарь с результатами для обновления.
"""
if rack_data.ventilation_panel is None:
return
try:
checkbox = self.get_content_item("ventilation_checkbox")
if not checkbox:
return
if rack_data.ventilation_panel:
checkbox.check(force=True)
else:
checkbox.uncheck(force=True)
results["checkboxes_set"] += 1
logger.info(f"Checkbox 'Ventilation panel' set to: {rack_data.ventilation_panel}")
except Exception as e:
logger.error(f"Error setting checkbox: {e}")
# Проверки # Проверки
def verify_all_filled_fields( def verify_all_filled_fields(
self, self,
rack_data: RackEditData, rack_data: EditRackData,
skip_fields: Optional[List[str]] = None skip_fields: Optional[List[str]] = None
) -> dict: ) -> dict:
"""Проверяет, что все поля заполнены корректно. """Проверяет, что все поля заполнены корректно.
@ -1193,8 +916,13 @@ class ModalEditRack(ModalWindowComponent):
if skip_fields is None: if skip_fields is None:
skip_fields = [] skip_fields = []
# Проверяем текстовые поля
self._verify_text_fields(rack_data, skip_fields, results) self._verify_text_fields(rack_data, skip_fields, results)
# Проверяем combobox поля
self._verify_combobox_fields(rack_data, skip_fields, results) self._verify_combobox_fields(rack_data, skip_fields, results)
# Проверяем чекбокс
self._verify_checkbox(rack_data, skip_fields, results) self._verify_checkbox(rack_data, skip_fields, results)
if results["total_expected_fields"] > 0: if results["total_expected_fields"] > 0:
@ -1208,7 +936,7 @@ class ModalEditRack(ModalWindowComponent):
def _verify_text_fields( def _verify_text_fields(
self, self,
rack_data: RackEditData, rack_data: EditRackData,
skip_fields: List[str], skip_fields: List[str],
results: dict results: dict
) -> None: ) -> None:
@ -1220,7 +948,11 @@ class ModalEditRack(ModalWindowComponent):
results: Словарь с результатами для обновления. results: Словарь с результатами для обновления.
""" """
for field_label, (attr_name, field_name) in self.TEXT_FIELDS_MAPPING.items(): 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():
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():
continue continue
@ -1250,7 +982,7 @@ class ModalEditRack(ModalWindowComponent):
""" """
try: try:
input_field = self.get_content_item(field_name) input_field = self.edit_form.get_content_item(field_name)
if not input_field: if not input_field:
results["not_filled"] += 1 results["not_filled"] += 1
results["field_errors"].append(f"Field '{field_label}' input not found") results["field_errors"].append(f"Field '{field_label}' input not found")
@ -1270,7 +1002,7 @@ class ModalEditRack(ModalWindowComponent):
def _verify_combobox_fields( def _verify_combobox_fields(
self, self,
rack_data: RackEditData, rack_data: EditRackData,
skip_fields: List[str], skip_fields: List[str],
results: dict results: dict
) -> None: ) -> None:
@ -1282,7 +1014,11 @@ class ModalEditRack(ModalWindowComponent):
results: Словарь с результатами для обновления. results: Словарь с результатами для обновления.
""" """
for field_label, (attr_name, _, _) in self.COMBOBOX_FIELDS_MAPPING.items(): 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():
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():
continue continue
@ -1335,8 +1071,12 @@ class ModalEditRack(ModalWindowComponent):
Значение поля или пустая строка. Значение поля или пустая строка.
""" """
if not self.edit_form:
return ""
actual_value = "" actual_value = ""
locator = self.COMBOBOX_FIELDS_LOCATORS.get(field_label) # Используем локаторы из edit_form
locator = self.edit_form.COMBOBOX_FIELDS_LOCATORS.get(field_label)
if not locator: if not locator:
return actual_value return actual_value
@ -1357,7 +1097,7 @@ class ModalEditRack(ModalWindowComponent):
def _verify_checkbox( def _verify_checkbox(
self, self,
rack_data: RackEditData, rack_data: EditRackData,
skip_fields: List[str], skip_fields: List[str],
results: dict results: dict
) -> None: ) -> None:
@ -1379,7 +1119,7 @@ class ModalEditRack(ModalWindowComponent):
return return
try: try:
checkbox = self.get_content_item("ventilation_checkbox") checkbox = self.edit_form.get_content_item("ventilation_checkbox")
if not checkbox: if not checkbox:
results["not_filled"] += 1 results["not_filled"] += 1
results["field_errors"].append("Checkbox 'Ventilation panel' not found") results["field_errors"].append("Checkbox 'Ventilation panel' not found")

View File

@ -1,355 +0,0 @@
"""Модуль страницы создания дочернего элемента.
Содержит класс для работы с формой создания дочернего элемента.
"""
from playwright.sync_api import Page, expect
from elements.tooltip_button_element import TooltipButton
from components.toolbar_component import ToolbarComponent
from components.dropdown_list_component import DropdownList
from pages.base_page import BasePage
from tools.logger import get_logger
logger = get_logger("CREATE_CHILD_ELEMENT")
# =============== Локаторы ================================================
PANEL_HEADER = "//span[text()='Объекты']/following-sibling::i"
TOOLBAR_CONTENT = "//div[@class='v-toolbar__content']"
CREATE_BUTTON_ANCESTOR_DIV3 = "xpath=/ancestor::div[3]//button"
PANEL_HEADER_ANCESTOR_DIV2 = "xpath=/ancestor::div[2]"
CREATE_CHILD_TITLE = "//div[contains(@class, 'v-toolbar__title') and contains(., 'Создать дочерний элемент в')]"
OBJECT_CLASS_COMBOBOX = "//div[@role='combobox' and .//label[text()='Класс объекта учета']]"
CANCEL_BUTTON = "//div[contains(@class, 'v-toolbar__title') and contains(., 'Создать дочерний элемент в')]/..//button[contains(@class, 'v-btn--icon')]"
# Локаторы для работы с combobox
COMBOBOX_LABEL = "label"
COMBOBOX_INPUT = "input[name='entity']"
COMBOBOX_ICON = ".v-input__icon--append"
COMBOBOX_ICON_ARROW = ".v-input__icon--append .mdi-menu-down"
# Локаторы для выпадающего списка combobox - уточненные
LISTBOX_SELECTOR = "//div[contains(@class, 'v-menu__content')]//div[@role='list']"
OPTIONS_SELECTOR = "//div[contains(@class, 'v-menu__content')]//div[@role='listitem']//span"
# Локаторы для получения выбранного значения
SELECTED_VALUE_SPAN = "span"
#========================================================================================================
class CreateChildElementTab(BasePage):
"""Класс для работы с формой создания дочернего элемента."""
def __init__(self, page: Page) -> None:
"""
Инициализирует объект формы создания дочернего элемента.
Args:
page: Экземпляр страницы Playwright
"""
super().__init__(page)
# Локаторы для кнопок
panel_header_locator = self.page.locator(PANEL_HEADER)
# Кнопка "Создать" - первая кнопка в тулбаре
create_button_locator = panel_header_locator.locator(CREATE_BUTTON_ANCESTOR_DIV3).nth(0)
# Кнопка "Отменить" - ищем глобально на странице
cancel_button_locator = self.page.locator(CANCEL_BUTTON)
# Инициализация кнопок
self.create_button = TooltipButton(page, create_button_locator, "add")
self.cancel_button = TooltipButton(page, cancel_button_locator, "cancel")
# Инициализация тулбара с обеими кнопками
self.toolbar = ToolbarComponent(page, "")
self.toolbar.add_tooltip_button(create_button_locator, "add")
self.toolbar.add_tooltip_button(cancel_button_locator, "cancel")
# Инициализация компонента выпадающего списка
self.dropdown = DropdownList(page)
def get_toolbar_title(self) -> list[str]:
"""
Получает заголовок панели инструментов.
Returns:
list[str]: Список элементов заголовка панели инструментов
"""
toolbar_title_locator = self.page.locator(PANEL_HEADER).\
locator(PANEL_HEADER_ANCESTOR_DIV2).get_by_role("navigation").\
locator(TOOLBAR_CONTENT)
return self.toolbar.get_toolbar_composite_title_text(toolbar_title_locator)
def should_be_toolbar_buttons(self) -> None:
"""
Проверяет наличие и функциональность кнопок тулбара.
Raises:
AssertionError: Если кнопки недоступны или подсказки неверны.
"""
self.wait_for_timeout(2000)
self.toolbar.check_button_visibility("cancel")
self.toolbar.check_button_tooltip("cancel", "Отменить")
self.toolbar.get_button_by_name("cancel").click()
self.wait_for_timeout(2000)
def click_create_button(self) -> None:
"""
Кликает на кнопку 'Создать'.
"""
logger.info("Клик на кнопку 'Создать'...")
self.toolbar.get_button_by_name("add").click()
def click_cancel_button(self) -> None:
"""
Кликает на кнопку 'Отменить'.
"""
logger.info("Клик на кнопку 'Отменить'...")
self.toolbar.get_button_by_name("cancel").click()
def check_toolbar_title(self, expected_title: str) -> None:
"""
Проверяет заголовок тулбара.
Args:
expected_title: Ожидаемый заголовок тулбара
Raises:
AssertionError: Если заголовок не соответствует ожидаемому
"""
# Используем метод тулбара с нашим специфичным локатором
self.toolbar.check_toolbar_presence_by_locator(CREATE_CHILD_TITLE,
f"Заголовок тулбара '{expected_title}' не найден")
# Получаем текст и проверяем его
actual_text = self.toolbar.get_toolbar_title_text(CREATE_CHILD_TITLE)
assert expected_title in actual_text, f"Заголовок не совпадает. Ожидалось: '{expected_title}', Получено: '{actual_text}'"
logger.info(f"Заголовок тулбара корректен: '{actual_text}'")
def check_object_class_combobox_presence(self) -> None:
"""
Проверяет наличие combobox 'Класс объекта учета'.
Raises:
AssertionError: Если combobox не найден
"""
logger.info("Проверка наличия combobox 'Класс объекта учета'...")
combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX)
expect(combobox_locator).to_be_visible()
logger.info("Combobox 'Класс объекта учета' найден")
def check_object_class_combobox_content(self) -> None:
"""
Проверяет содержимое combobox 'Класс объекта учета'.
Raises:
AssertionError: Если содержимое не соответствует ожидаемому
"""
logger.info("Проверка содержимого combobox 'Класс объекта учета'...")
combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX)
# Проверяем что combobox видим
expect(combobox_locator).to_be_visible()
# Проверяем наличие label
label_locator = combobox_locator.locator(COMBOBOX_LABEL)
expect(label_locator).to_have_text("Класс объекта учета")
# Проверяем наличие input поля
input_locator = combobox_locator.locator(COMBOBOX_INPUT)
expect(input_locator).to_be_visible()
# Для combobox нормально иметь readonly атрибут - это стандартное поведение
# Проверяем что поле доступно для выбора (не disabled)
expect(input_locator).not_to_have_attribute("disabled", "disabled")
# Проверяем наличие иконки стрелки
icon_locator = combobox_locator.locator(COMBOBOX_ICON_ARROW)
expect(icon_locator).to_be_visible()
logger.info("Содержимое combobox 'Класс объекта учета' корректно")
def open_object_class_combobox(self) -> None:
"""
Открывает выпадающий список combobox 'Класс объекта учета'.
"""
logger.info("Открытие combobox 'Класс объекта учета'...")
combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX)
listbox_locator = self.page.locator(LISTBOX_SELECTOR)
icon_locator = combobox_locator.locator(COMBOBOX_ICON)
# Проверяем, не открыт ли уже список
listbox_already_open = False
listbox_count = listbox_locator.count()
if listbox_count > 0:
listbox_already_open = listbox_locator.first.is_visible()
if not listbox_already_open:
# Только если список не открыт, кликаем на иконку
icon_locator.click(timeout=10000)
logger.info("Клик на иконку combobox выполнен")
self.wait_for_timeout(1000)
# Проверяем что список открылся
listbox_count_after = listbox_locator.count()
listbox_visible = False
if listbox_count_after > 0:
listbox_visible = listbox_locator.first.is_visible()
if listbox_visible:
logger.info("Выпадающий список найден и открыт")
else:
logger.warning("Не удалось открыть выпадающий список")
def get_object_class_options(self) -> list[str]:
"""
Получает список доступных опций из combobox.
Returns:
list[str]: Список доступных классов объектов
"""
logger.info("Получение списка опций combobox 'Класс объекта учета'...")
# Открываем combobox (если еще не открыт)
self.open_object_class_combobox()
# Используем метод get_item_names из DropdownList
options_list = self.dropdown.get_item_names(LISTBOX_SELECTOR)
# Закрываем combobox (кликаем вне его)
self.page.mouse.click(10, 10)
self.wait_for_timeout(500)
logger.info(f"Найдено опций: {len(options_list)} - {options_list}")
return options_list
def select_object_class(self, class_name: str) -> None:
"""
Выбирает класс объекта из выпадающего списка.
Args:
class_name: Название класса объекта для выбора
Raises:
AssertionError: Если класс не найден в списке
"""
logger.info(f"Выбор класса объекта: '{class_name}'...")
# Открываем combobox
self.open_object_class_combobox()
self.dropdown.click_item_with_text(class_name)
# Проверяем что выбор произошел
self.wait_for_timeout(1000)
selected_value = self.get_selected_object_class()
if class_name.lower() not in selected_value.lower() and selected_value.lower() not in class_name.lower():
# Если выбор не произошел, получаем доступные опции для отладки
available_options = self.get_object_class_options()
logger.warning(f"Класс '{class_name}' не выбран. Текущее значение: '{selected_value}'. Доступные опции: {available_options}")
raise AssertionError(f"Не удалось выбрать класс объекта '{class_name}'")
logger.info(f"Класс объекта '{class_name}' успешно выбран")
def get_selected_object_class(self) -> str:
"""
Получает выбранный класс объекта учета.
Returns:
str: Выбранный класс объекта или пустая строка если ничего не выбрано
"""
combobox_locator = self.page.locator(OBJECT_CLASS_COMBOBOX)
selected_value = ""
# Ищем в span элементах
span_locator = combobox_locator.locator(SELECTED_VALUE_SPAN)
if span_locator.count() > 0:
for i in range(span_locator.count()):
span_text = span_locator.nth(i).text_content().strip()
if span_text and span_text not in ["Класс объекта учета"]:
selected_value = span_text
break
logger.info(f"Выбранный класс объекта: '{selected_value}'")
return selected_value
def check_object_class_selected(self, expected_class: str) -> None:
"""
Проверяет что выбран указанный класс объекта.
Args:
expected_class: Ожидаемый выбранный класс объекта
Raises:
AssertionError: Если выбранный класс не соответствует ожидаемому
"""
logger.info(f"Проверка выбранного класса объекта: '{expected_class}'...")
# Даем время на обновление значения
self.wait_for_timeout(1000)
actual_class = self.get_selected_object_class()
# Проверка - допускаем частичное совпадение
if expected_class.lower() in actual_class.lower() or actual_class.lower() in expected_class.lower():
logger.info(f"Класс объекта '{expected_class}' успешно выбран (фактически: '{actual_class}')")
else:
raise AssertionError(f"Выбранный класс не соответствует ожидаемому. Ожидалось: '{expected_class}', Получено: '{actual_class}'")
def check_object_class_options_content(self, expected_options: list = None) -> None:
"""
Проверяет содержимое списка опций combobox.
Args:
expected_options: Ожидаемый список опций. Если None, проверяет только что список не пустой.
Raises:
AssertionError: Если список опций не соответствует ожидаемому
"""
logger.info("Проверка содержимого списка опций combobox...")
# Получаем доступные опции
available_options = self.get_object_class_options()
if expected_options is not None:
# Проверяем соответствие ожидаемому списку
assert set(available_options) == set(expected_options), (
f"Список опций не соответствует ожидаемому. "
f"Ожидалось: {expected_options}, Получено: {available_options}"
)
else:
# Проверяем что список не пустой
assert len(available_options) > 0, "Список опций combobox пустой"
logger.info(f"Содержимое списка опций корректно: {available_options}")
def check_dropdown_item_presence(self, item_text: str) -> None:
"""
Проверяет наличие элемента в выпадающем списке.
Args:
item_text: Текст элемента для проверки
"""
logger.info(f"Проверка наличия элемента '{item_text}' в выпадающем списке...")
# Получаем все опции и проверяем наличие
available_options = self.get_object_class_options()
if item_text not in available_options:
raise AssertionError(f"Элемент '{item_text}' не найден в списке опций. Доступные опции: {available_options}")
logger.info(f"Элемент '{item_text}' присутствует в списке")

View File

@ -1,678 +0,0 @@
"""Модуль страницы создания дочернего элемента.
Содержит класс для работы с формой создания дочернего элемента.
"""
import re
from playwright.sync_api import Page, expect
from elements.tooltip_button_element import TooltipButton
from components.toolbar_component import ToolbarComponent
from components_derived.selection_bar_component import SelectionBarComponent
from pages.main_page import MainPage
from pages.base_page import BasePage
from components.base_component import BaseComponent
from components.alert_component import AlertComponent
from components.navbar_component import NavigationPanelComponent
from locators.navigation_panel_locators import NavigationPanelLocators
from locators.combobox_locators import ComboboxLocators
from locators.rack_locators import RackLocators
from locators.alert_locators import AlertLocators
from tools.logger import get_logger
logger = get_logger("CREATE_RACK_ELEMENT")
# Словарь для сопоставления названий полей с локаторами
COMBOBOX_FIELDS_MAP = {
# Обязательные поля
"Имя": RackLocators.RACK_NAME_FIELD,
"Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD,
"Глубина (мм)": RackLocators.RACK_DEPTH_FIELD,
# Дополнительные текстовые поля
"Серийный номер": RackLocators.RACK_SERIAL_FIELD,
"Инвентарный номер": RackLocators.RACK_INVENTORY_FIELD,
"Комментарий": RackLocators.RACK_COMMENT_FIELD,
# Combobox поля
"Ввод кабеля": RackLocators.RACK_CABLE_ENTRY_FIELD,
"Состояние": RackLocators.RACK_STATE_FIELD,
"Владелец": RackLocators.RACK_OWNER_FIELD,
"Обслуживающая организация": RackLocators.RACK_SERVICE_ORG_FIELD,
"Проект/Титул": RackLocators.RACK_PROJECT_FIELD
}
class CreateRackElementTab(BasePage):
"""Класс для работы с формой создания дочернего элемента."""
def __init__(self, page: Page) -> None:
"""
Инициализирует объект формы создания дочернего элемента.
Args:
page: Экземпляр страницы Playwright
"""
super().__init__(page)
# Инициализация BaseComponent
self.base_component = BaseComponent(page)
# Инициализация AlertComponent
self.alert = AlertComponent(page)
# Инициализация MainPage для работы с навигацией
self.main_page = MainPage(page)
# Инициализация NavigationPanelComponent
self.navigation_panel = NavigationPanelComponent(page)
# Кнопка "Добавить" - первая кнопка в тулбаре
create_button_locator = self.page.get_by_role("navigation").filter(has_text=re.compile('Создать дочерний элемент в')).get_by_role("button").nth(0)
# Кнопка "Отменить" - вторая кнопка в тулбаре
cancel_button_locator = self.page.get_by_role("navigation").filter(has_text=re.compile('Создать дочерний элемент в')).get_by_role("button").nth(1)
# Инициализация кнопок
self.create_button = TooltipButton(page, create_button_locator, "add")
self.cancel_button = TooltipButton(page, cancel_button_locator, "cancel")
# Инициализация тулбара с обеими кнопками
self.toolbar = ToolbarComponent(page, "Создать дочерний элемент в")
self.toolbar.add_tooltip_button(create_button_locator, "add")
self.toolbar.add_tooltip_button(cancel_button_locator, "cancel")
# Инициализация компонента панели выбора значения для работы с combobox
self.selection_bar = SelectionBarComponent(page, ComboboxLocators.OBJECT_CLASS_COMBOBOX)
# =============== МЕТОДЫ ДЕЙСТВИЙ ========================
def click_add_button(self) -> None:
"""
Кликает на кнопку 'Добавить'.
"""
self.toolbar.click_button("add")
def click_cancel_button(self) -> None:
"""
Кликает на кнопку 'Отменить'.
"""
self.toolbar.click_button("cancel")
def open_object_class_combobox(self) -> None:
"""
Открывает выпадающий список combobox 'Класс объекта учета'.
"""
logger.info("Открытие combobox 'Класс объекта учета'...")
self.selection_bar.open_values_list()
def select_object_class(self, class_name: str) -> None:
"""
Выбирает класс объекта из выпадающего списка.
Args:
class_name: Название класса объекта для выбора
Raises:
AssertionError: Если класс не найден в списке
"""
logger.info(f"Выбор класса объекта: '{class_name}'...")
# Открываем combobox
self.open_object_class_combobox()
# Выбираем значение из списка
self.selection_bar.select_value(class_name)
# Проверяем что выбор произошел
self.wait_for_timeout(1000)
selected_value = self.get_selected_object_class()
if class_name.lower() not in selected_value.lower() and selected_value.lower() not in class_name.lower():
# Если выбор не произошел, получаем доступные опции для отладки
available_options = self.get_object_class_options()
logger.warning(f"Класс '{class_name}' не выбран. Текущее значение: '{selected_value}'. Доступные опции: {available_options}")
raise AssertionError(f"Не удалось выбрать класс объекта '{class_name}'")
logger.info(f"Класс объекта '{class_name}' успешно выбран")
def get_object_class_options(self) -> list[str]:
"""
Получает список доступных опций из combobox.
Returns:
list[str]: Список доступных классов объектов
"""
logger.info("Получение списка опций combobox 'Класс объекта учета'...")
available_options = self.selection_bar.get_available_options()
logger.info(f"Доступные опции класса объекта: {available_options}")
return available_options
def get_selected_object_class(self) -> str:
"""
Получает выбранный класс объекта учета.
Returns:
str: Выбранный класс объекта или пустая строка если ничего не выбрано
"""
# Получаем заголовок панели выбора
return self.selection_bar.get_selection_bar_title()
def fill_rack_data(self, name: str, height: str = "42", depth: str = "1000",
serial: str = "", inventory: str = "", comment: str = "",
cable_entry: str = "", state: str = "", owner: str = "",
service_org: str = "", project: str = "") -> None:
"""
Заполняет данные для создания стойки.
Args:
name: Наименование стойки
height: Высота в юнитах (по умолчанию 42)
depth: Глубина в мм (по умолчанию 1000)
serial: Серийный номер
inventory: Инвентарный номер
comment: Комментарий
cable_entry: Ввод кабеля
state: Состояние
owner: Владелец
service_org: Обслуживающая организация
project: Проект/Титул
"""
logger.info(f"Заполнение данных стойки: {name}")
# Заполняем обязательные поля
name_field = self.page.locator(RackLocators.RACK_NAME_FIELD)
name_field.fill(name)
logger.info(f"Заполнено поле 'Имя': {name}")
self._select_combobox("Высота в юнитах", height)
logger.info(f"Выбрана высота: {height} юнитов")
self._select_combobox("Глубина (мм)", depth)
logger.info(f"Выбрана глубина: {depth} мм")
# Заполняем опциональные поля
if serial:
serial_field = self.page.locator(RackLocators.RACK_SERIAL_FIELD)
serial_field.fill(serial)
logger.info(f"Заполнен серийный номер: {serial}")
if inventory:
inventory_field = self.page.locator(RackLocators.RACK_INVENTORY_FIELD)
inventory_field.fill(inventory)
logger.info(f"Заполнен инвентарный номер: {inventory}")
if comment:
comment_field = self.page.locator(RackLocators.RACK_COMMENT_FIELD)
comment_field.fill(comment)
logger.info(f"Добавлен комментарий: {comment}")
# Заполняем дополнительные combobox поля
if cable_entry:
self._select_combobox("Ввод кабеля", cable_entry)
logger.info(f"Выбран ввод кабеля: {cable_entry}")
if state:
self._select_combobox("Состояние", state)
logger.info(f"Выбрано состояние: {state}")
if owner:
self._select_combobox("Владелец", owner)
logger.info(f"Выбран владелец: {owner}")
if service_org:
self._select_combobox("Обслуживающая организация", service_org)
logger.info(f"Выбрана обслуживающая организация: {service_org}")
if project:
self._select_combobox("Проект/Титул", project)
logger.info(f"Выбран проект/титул: {project}")
logger.info("Данные стойки заполнены")
def _select_combobox(self, field_name: str, value: str) -> None:
"""
Выбор значения в combobox.
Args:
field_name: Название поля
value: Значение для выбора
"""
logger.info(f"Выбор '{value}' в поле '{field_name}'...")
# Получаем статический локатор из словаря
if field_name not in COMBOBOX_FIELDS_MAP:
raise ValueError(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP")
field_locator = COMBOBOX_FIELDS_MAP[field_name]
# Для всех полей используем first() чтобы избежать strict mode violation
field_container = self.page.locator(field_locator).first
# Прокручиваем до поля
field_container.scroll_into_view_if_needed()
self.wait_for_timeout(500)
# Проверяем видимость поля
self.base_component.check_visibility(field_container, f"Поле '{field_name}' не найдено")
# Универсальный клик с force=True для всех полей
field_container.click(force=True)
self.wait_for_timeout(1000)
# Вводим значение
self.page.keyboard.type(value)
self.wait_for_timeout(500)
self.page.keyboard.press("Enter")
logger.info(f"Поле '{field_name}' заполнено")
def create_rack(self, rack_name: str, **kwargs) -> None:
"""
Полный процесс создания стойки.
Args:
rack_name: Наименование стойки
**kwargs: Дополнительные параметры стойки
"""
logger.info(f"Начало процесса создания стойки: {rack_name}")
# Выбираем класс объекта "Стойка"
self.select_object_class("Стойка")
self.wait_for_timeout(1000)
# Проверяем наличие полей стойки
self.check_rack_fields_presence()
# Заполняем данные
self.fill_rack_data(rack_name, **kwargs)
# Создаем стойку
self.click_add_button()
logger.info(f"Процесс создания стойки '{rack_name}' завершен")
def clear_combobox_field(self, field_name: str) -> None:
"""
Очищает значение в combobox поле с помощью кнопки закрытия (крестика).
Args:
field_name: Название поля для очистки
"""
logger.info(f"Очистка combobox поля '{field_name}' с помощью кнопки закрытия...")
if field_name not in COMBOBOX_FIELDS_MAP:
logger.warning(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP")
return
field_locator = COMBOBOX_FIELDS_MAP[field_name]
# Находим поле по локатору
field_container = self.page.locator(field_locator).first
# Проверяем что поле видимо
if not field_container.is_visible():
logger.info(f"Поле '{field_name}' не видимо, пропускаем очистку")
return
# Прокручиваем до поля
field_container.scroll_into_view_if_needed()
self.wait_for_timeout(500)
# Ищем кнопку закрытия (крестик) внутри контейнера поля
close_button = field_container.locator(ComboboxLocators.COMBOBOX_CLOSE_BUTTON)
# Проверяем наличие и видимость кнопки закрытия
if close_button.count() > 0 and close_button.is_visible():
# Если кнопка закрытия видима - кликаем на нее
close_button.click()
self.wait_for_timeout(500)
logger.info(f"Combobox поле '{field_name}' очищено с помощью кнопки закрытия")
else:
# Если кнопки закрытия нет, просто логируем этот факт
logger.info(f"Кнопка закрытия не найдена для поля '{field_name}', очистка не выполнена")
def clear_rack_fields(self) -> None:
"""
Очищает все поля формы создания стойки.
"""
logger.info("Очистка всех полей формы стойки...")
# Очищаем текстовые поля
text_fields = [
(RackLocators.RACK_NAME_FIELD, "Имя"),
(RackLocators.RACK_SERIAL_FIELD, "Серийный номер"),
(RackLocators.RACK_INVENTORY_FIELD, "Инвентарный номер"),
(RackLocators.RACK_COMMENT_FIELD, "Комментарий")
]
for field_locator, field_name in text_fields:
field = self.page.locator(field_locator)
if field.count() > 0 and field.first.is_visible():
field.fill("")
logger.info(f"Текстовое поле '{field_name}' очищено")
# Очищаем combobox поля
combobox_fields = [
"Высота в юнитах",
"Глубина (мм)",
"Ввод кабеля",
"Состояние",
"Владелец",
"Обслуживающая организация",
"Проект/Титул"
]
for field_name in combobox_fields:
self.clear_combobox_field(field_name)
logger.info("Все поля формы стойки очищены")
# =============== МЕТОДЫ ПРОВЕРОК ========================
def check_rack_exists(self, rack_name: str) -> bool:
"""
Проверяет, существует ли уже стойка с указанным именем в навигационной панели.
Args:
rack_name: Имя стойки для проверки
Returns:
bool: True если стойка существует, False если нет
"""
logger.info(f"Проверка существования стойки с именем '{rack_name}'")
self.main_page.click_main_navigation_panel_item("Объекты")
self.main_page.click_main_navigation_panel_item("Объекты")
self.wait_for_timeout(1000)
self.main_page.click_subpanel_item("test-zone")
self.wait_for_timeout(3000)
nav_panel_locator = NavigationPanelLocators.TREEVIEW
# Проверяем видимость элемента через is_visible
element = self.page.locator(nav_panel_locator).get_by_text(rack_name).first
if element.is_visible():
logger.info(f"Стойка с именем '{rack_name}' найдена")
return True
else:
logger.info(f"Стойки с именем '{rack_name}' не найдена")
return False
def should_be_toolbar_buttons(self) -> None:
"""
Проверяет наличие и функциональность кнопок тулбара.
Raises:
AssertionError: Если кнопки недоступны или подсказки неверны.
"""
self.wait_for_timeout(2000)
self.toolbar.check_button_visibility("add")
self.toolbar.check_button_tooltip("add", "Добавить")
self.toolbar.check_button_visibility("cancel")
self.toolbar.check_button_tooltip("cancel", "Отменить")
self.toolbar.click_button("cancel")
self.wait_for_timeout(2000)
def check_toolbar_title(self, expected_title: str) -> None:
"""
Проверяет заголовок тулбара.
Args:
expected_title: Ожидаемый заголовок тулбара
Raises:
AssertionError: Если заголовок не соответствует ожидаемому
"""
logger.info(f"Проверка заголовок тулбара: '{expected_title}'...")
# Используем метод тулбара с фильтрацией по тексту
actual_text = self.toolbar.get_toolbar_title_text(
filter_text="Создать дочерний элемент в"
)
assert expected_title in actual_text, f"Заголовок не совпадает. Ожидалось: '{expected_title}', Получено: '{actual_text}'"
logger.info(f"Заголовок тулбара корректен: '{actual_text}'")
def check_object_class_combobox_presence(self) -> None:
"""
Проверяет наличие combobox 'Класс объекта учета'.
Raises:
AssertionError: Если combobox не найден
"""
logger.info("Проверка наличия combobox 'Класс объекта учета'...")
self.base_component.check_visibility(ComboboxLocators.OBJECT_CLASS_COMBOBOX, "Combobox 'Класс объекта учета' не найден")
logger.info("Combobox 'Класс объекта учета' найден")
def check_object_class_combobox_content(self) -> None:
"""
Проверяет содержимое combobox 'Класс объекта учета'.
Raises:
AssertionError: Если содержимое не соответствует ожидаемому
"""
logger.info("Проверка содержимого combobox 'Класс объекта учета'...")
combobox_locator = self.page.locator(ComboboxLocators.OBJECT_CLASS_COMBOBOX)
# Проверяем что combobox видим
self.base_component.check_visibility(ComboboxLocators.OBJECT_CLASS_COMBOBOX, "Combobox 'Класс объекта учета' не виден")
# Проверяем наличие label
label_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_LABEL)
expect(label_locator).to_have_text("Класс объекта учета")
# Проверяем наличие input поля
input_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_INPUT)
self.base_component.check_visibility(input_locator, "Input поле combobox не найдено")
# Для combobox нормально иметь readonly атрибут - это стандартное поведение
# Проверяем что поле доступно для выбора (не disabled)
expect(input_locator).not_to_have_attribute("disabled", "disabled")
# Проверяем наличие иконки стрелки
icon_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_ICON_ARROW)
self.base_component.check_visibility(icon_locator, "Иконка стрелки combobox не найдена")
logger.info("Содержимое combobox 'Класс объекта учета' корректно")
def check_object_class_selected(self, expected_class: str) -> None:
"""
Проверяет что выбран указанный класс объекта.
Args:
expected_class: Ожидаемый выбранный класс объекта
Raises:
AssertionError: Если выбранный класс не соответствует ожидаемому
"""
logger.info(f"Проверка выбранного класса объекта: '{expected_class}'...")
# Даем время на обновление значения
self.wait_for_timeout(1000)
actual_class = self.get_selected_object_class()
# Проверка - допускаем частичное совпадение
if expected_class.lower() in actual_class.lower() or actual_class.lower() in expected_class.lower():
logger.info(f"Класс объекта '{expected_class}' успешно выбран (фактически: '{actual_class}')")
else:
raise AssertionError(f"Выбранный класс не соответствует ожидаемому. Ожидалось: '{expected_class}', Получено: '{actual_class}'")
def check_object_class_options_content(self, expected_options: list = None) -> None:
"""
Проверяет содержимое списка опций combobox.
Args:
expected_options: Ожидаемый список опций. Если None, проверяет только что список не пустой.
Raises:
AssertionError: Если список опций не соответствует ожидаемому
"""
logger.info("Проверка содержимого списка опций combobox...")
# Получаем доступные опции
available_options = self.get_object_class_options()
if expected_options is not None:
# Проверяем соответствие ожидаемому списку
assert set(available_options) == set(expected_options), (
f"Список опций не соответствует ожидаемому. "
f"Ожидалось: {expected_options}, Получено: {available_options}"
)
else:
# Проверяем что список не пустой
assert len(available_options) > 0, "Список опций combobox пустой"
logger.info(f"Содержимое списка опций корректно: {available_options}")
def check_dropdown_item_presence(self, item_text: str) -> None:
"""
Проверяет наличие элемента в выпадающем списке.
Args:
item_text: Текст элемента для проверки
"""
logger.info(f"Проверка наличия элемента '{item_text}' в выпадающем списке...")
# Получаем все опции и проверяем наличие
available_options = self.get_object_class_options()
if item_text not in available_options:
raise AssertionError(f"Элемент '{item_text}' не найден в списке опций. Доступные опции: {available_options}")
logger.info(f"Элемент '{item_text}' присутствует в списке")
def check_rack_fields_presence(self) -> None:
"""
Проверяет наличие полей специфичных для стойки.
Raises:
AssertionError: Если какое-либо поле не найдено
"""
logger.info("Проверка наличия полей для стойки...")
# Основные обязательные поля
required_fields = [
(RackLocators.RACK_NAME_FIELD, "Имя"),
(RackLocators.RACK_HEIGHT_FIELD, "Высота в юнитах"),
(RackLocators.RACK_DEPTH_FIELD, "Глубина (мм)")
]
# Дополнительные поля
optional_fields = [
(RackLocators.RACK_SERIAL_FIELD, "Серийный номер"),
(RackLocators.RACK_INVENTORY_FIELD, "Инвентарный номер"),
(RackLocators.RACK_COMMENT_FIELD, "Комментарий"),
(RackLocators.RACK_CABLE_ENTRY_FIELD, "Ввод кабеля"),
(RackLocators.RACK_STATE_FIELD, "Состояние"),
(RackLocators.RACK_OWNER_FIELD, "Владелец"),
(RackLocators.RACK_SERVICE_ORG_FIELD, "Обслуживающая организация"),
(RackLocators.RACK_PROJECT_FIELD, "Проект/Титул")
]
# Проверяем обязательные поля
for field_locator, field_name in required_fields:
self.base_component.check_visibility(field_locator, f"Обязательное поле '{field_name}' не найдено")
logger.info(f"Обязательное поле '{field_name}' найдено")
# Проверяем дополнительные поля
for field_locator, field_name in optional_fields:
field = self.page.locator(field_locator)
if field.count() > 0 and field.first.is_visible():
logger.info(f"Дополнительное поле '{field_name}' найдено")
else:
logger.info(f"Дополнительное поле '{field_name}' не найдено или не отображается")
logger.info("Все основные поля для стойки присутствуют")
def check_field_highlighted_error(self, field_name: str) -> None:
"""
Проверяет, что поле подсвечено цветом ошибки (валидация не пройдена).
Args:
field_name: Название поля для проверки
"""
logger.info(f"Проверка подсветки поля '{field_name}' цветом ошибки...")
# Локаторы только для обязательных полей
required_fields = {
"Имя": RackLocators.RACK_NAME_FIELD,
"Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD,
"Глубина (мм)": RackLocators.RACK_DEPTH_FIELD
}
if field_name not in required_fields:
raise ValueError(f"Поле '{field_name}' не является обязательным или не поддерживается")
field_locator = required_fields[field_name]
field_element = self.page.locator(field_locator)
# Проверяем что поле видимо
self.base_component.check_visibility(field_element, f"Поле '{field_name}' не найдено")
# Ищем родительский контейнер с использованием константы
parent_container = field_element.locator(RackLocators.INPUT_PARENT_CONTAINER).first
# Проверка классов ошибки
if parent_container.count() > 0:
error_classes = AlertLocators.ERROR_CLASSES
is_error_highlighted = False
for error_class in error_classes:
error_element = parent_container.locator(f".{error_class}")
if error_element.count() > 0:
is_error_highlighted = True
logger.info(f"Поле '{field_name}' подсвечено ошибкой")
break
if not is_error_highlighted:
raise AssertionError(f"Поле '{field_name}' не подсвечено цветом ошибки ")
logger.info(f"Поле '{field_name}' корректно подсвечено цветом ошибки")
def check_field_not_highlighted_error(self, field_name: str) -> None:
"""
Проверяет, что поле НЕ подсвечено цветом ошибки (валидация успешна).
Args:
field_name: Название поля для проверки
"""
logger.info(f"Проверка отсутствия подсветки ошибки у поля '{field_name}'...")
# Локаторы только для обязательных полей
required_fields = {
"Имя": RackLocators.RACK_NAME_FIELD,
"Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD,
"Глубина (мм)": RackLocators.RACK_DEPTH_FIELD
}
if field_name not in required_fields:
raise ValueError(f"Поле '{field_name}' не является обязательным или не поддерживается")
field_locator = required_fields[field_name]
field_element = self.page.locator(field_locator)
# Проверяем что поле видимо
self.base_component.check_visibility(field_element, f"Поле '{field_name}' не найдено")
# Ищем родительский контейнер с использованием константы
parent_container = field_element.locator(RackLocators.INPUT_PARENT_CONTAINER).first
# Поверка отсутствия классов ошибки
if parent_container.count() > 0:
error_classes = AlertLocators.ERROR_CLASSES
for error_class in error_classes:
error_element = parent_container.locator(f".{error_class}")
if error_element.count() > 0:
raise AssertionError(f"Поле '{field_name}' подсвечено ошибкой")
logger.info(f"Поле '{field_name}' корректно не подсвечено цветом ошибки")

View File

@ -1,466 +0,0 @@
"""Модуль тестов вкладки 'Стойка'.
Содержит тесты для проверки функциональности
работы со стойкой оборудования.
"""
from playwright.sync_api import Page, expect
from elements.tooltip_button_element import TooltipButton
from components.toolbar_component import ToolbarComponent
from pages.base_page import BasePage
from locators.rack_locators import RackLocators
from tools.logger import get_logger
logger = get_logger("RACK_TAB")
# Специфичные локаторы оставленые в основном коде
PANEL_HEADER = "//span[text()='Объекты']/following-sibling::i"
TOOLBAR_CONTENT = "//div[@class='v-toolbar__content']"
EDIT_BUTTON_ANCESTOR_DIV3 = "xpath=/ancestor::div[3]//button"
PANEL_HEADER_ANCESTOR_DIV2 = "xpath=/ancestor::div[2]"
class RackTab(BasePage):
"""Класс для работы с вкладкой стойки оборудования."""
def __init__(self, page: Page) -> None:
"""
Инициализирует объект вкладки стойки.
Args:
page: Экземпляр страницы Playwright
"""
super().__init__(page)
locator_button = self.page.locator(PANEL_HEADER).\
locator(EDIT_BUTTON_ANCESTOR_DIV3).nth(0)
self.edit_button = TooltipButton(page, locator_button, "edit")
self.toolbar = ToolbarComponent(page, "")
self.toolbar.add_tooltip_button(locator_button, "edit")
def wait_for_rack_loading(self, timeout: int = 15000) -> None:
"""
Ожидает загрузки интерфейса стойки.
Args:
timeout: Время ожидания в миллисекундах (по умолчанию 15000)
Raises:
TimeoutError: Если загрузка не завершилась в указанное время
"""
logger.info("Ожидание загрузки интерфейса стойки...")
# Ждем появления основного контейнера
main_container = self.page.locator(RackLocators.MAIN_CONTAINER)
expect(main_container).to_be_visible(timeout=timeout)
# Ждем появления юнитов
units = self.page.locator(RackLocators.ALL_UNITS)
expect(units).to_have_count(20, timeout=timeout)
logger.info("Интерфейс стойки загружен")
def get_toolbar_title(self) -> list[str]:
"""
Получает заголовок панели инструментов.
Returns:
list[str]: Список элементов заголовка панели инструментов
"""
toolbar_title_locator = self.page.locator(PANEL_HEADER).\
locator(PANEL_HEADER_ANCESTOR_DIV2).get_by_role("navigation").\
locator(TOOLBAR_CONTENT)
return self.toolbar.get_toolbar_composite_title_text(toolbar_title_locator)
def switch_to_tab(self, tab_name: str) -> None:
"""
Переключается на указанную вкладку.
Args:
tab_name: Название вкладки для переключения
Raises:
AssertionError: Если вкладка не найдена или недоступна
"""
logger.info(f"Переключение на вкладку '{tab_name}'...")
tab = self.page.locator(RackLocators.TAB_BY_NAME.format(tab_name))
if tab.count() == 0:
raise AssertionError(f"Вкладка '{tab_name}' не найдена")
# Проверяем активность ДО клика
if self.is_tab_active(tab_name):
logger.info(f"Вкладка '{tab_name}' уже активна")
return
# Находим первую видимую вкладку с нужным именем
target_tab = None
for i in range(tab.count()):
element = tab.nth(i)
if element.is_visible() and element.is_enabled():
target_tab = element
break
if not target_tab:
raise AssertionError(f"Не найдена видимая/доступная вкладка '{tab_name}'")
# Кликаем на вкладку
logger.info(f"Клик на вкладку '{tab_name}'...")
target_tab.click()
# Ждем изменения активной вкладки
self._wait_for_tab_activation(tab_name)
# Ждем загрузки контента
self.page.wait_for_timeout(500)
def switch_to_general_info_tab(self) -> None:
"""Переключается на вкладку 'Общая информация'."""
self.switch_to_tab("Общая информация")
def switch_to_maintenance_tab(self) -> None:
"""Переключается на вкладку 'Обслуживание'."""
self.switch_to_tab("Обслуживание")
def switch_to_events_tab(self) -> None:
"""Переключается на вкладку 'События'."""
self.switch_to_tab("События")
def switch_to_services_tab(self) -> None:
"""Переключается на вкладку 'Сервисы'."""
self.switch_to_tab("Сервисы")
def is_tab_active(self, tab_name: str) -> bool:
"""
Проверяет, активна ли указанная вкладка.
Args:
tab_name: Название вкладки для проверки
Returns:
bool: True если вкладка активна, False в противном случае
"""
# Метод 1: Проверяем по активному классу и тексту, метод быстый, если надо универсальный оставояем метод 2 - медленный
active_tab = self.page.locator(RackLocators.ACTIVE_TAB)
if active_tab.count() > 0 and active_tab.first.is_visible():
active_text = active_tab.first.text_content()
if active_text and active_text.strip() == tab_name:
logger.info(f"Вкладка '{tab_name}' активна (через класс активной вкладки)")
return True
# Метод 2: Проверяем по классам у конкретной вкладки
tab = self.page.locator(RackLocators.TAB_BY_NAME.format(tab_name))
if tab.count() > 0:
for i in range(tab.count()):
element = tab.nth(i)
if element.is_visible() and element.is_enabled():
element_class = element.get_attribute("class") or ""
is_active = any(
active_class in element_class
for active_class in RackLocators.ACTIVE_TAB_CLASSES
)
if is_active:
logger.info(f"Вкладка '{tab_name}' активна (классы: {element_class})")
return True
logger.info(f"Вкладка '{tab_name}' не активна")
return False
def get_available_tabs(self) -> list[str]:
"""
Возвращает список доступных вкладок используя DOM структуру.
Returns:
list[str]: Список названий доступных вкладок
"""
tabs = []
# Используем локатор для верхних вкладок
tab_elements = self.page.locator(RackLocators.ALL_TABS)
# Ждем появления элементов
tab_elements.first.wait_for(state="visible", timeout=5000)
total_count = tab_elements.count()
logger.info(f"Всего найдено элементов верхних вкладок: {total_count}")
for i in range(total_count):
element = tab_elements.nth(i)
# Проверяем видимость и доступность элемента
if element.is_visible() and element.is_enabled():
tab_text = element.text_content()
if tab_text:
tab_text = tab_text.strip()
if tab_text and tab_text not in tabs:
tabs.append(tab_text)
logger.info(f"Найдена верхняя вкладка: '{tab_text}'")
logger.info(f"Найдены доступные верхние вкладки: {tabs}")
return tabs
def _wait_for_tab_activation(self, tab_name: str, timeout: int = 5000) -> None:
"""
Ожидает активации вкладки.
Args:
tab_name: Название вкладки для ожидания
timeout: Время ожидания в миллисекундах
Raises:
AssertionError: Если вкладка не активирована в течение таймаута
"""
logger.info(f"Ожидание активации вкладки '{tab_name}'...")
start_time = self.page.evaluate("Date.now()")
while self.page.evaluate("Date.now()") - start_time < timeout:
if self.is_tab_active(tab_name):
logger.info(f"Вкладка '{tab_name}' успешно активирована")
return
self.page.wait_for_timeout(100)
raise AssertionError(f"Вкладка '{tab_name}' не активирована в течение {timeout}мс")
def should_be_toolbar_buttons(self) -> None:
"""
Проверяет наличие и функциональность кнопок тулбара.
Raises:
AssertionError: Если кнопки недоступны или подсказки неверны.
"""
logger.info("Проверка кнопок панели инструментов...")
self.toolbar.check_button_visibility("edit")
self.toolbar.check_button_tooltip("edit", "Изменить")
self.toolbar.get_button_by_name("edit").click()
def should_be_rack_sides_displayed(self) -> None:
"""
Проверка отображения и структуры сторон стойки.
Raises:
AssertionError: Если стороны стойки не отображаются корректно
"""
logger.info("Проверка отображения и структуры сторон стойки...")
# Ожидаем загрузки
self.wait_for_rack_loading()
# БАЗОВАЯ ПРОВЕРКА: обе стороны отображаются
logger.info("--- Базовая проверка отображения сторон ---")
front_side_section = self.page.locator(RackLocators.FRONT_SIDE_SECTION).first
expect(front_side_section).to_be_visible(timeout=10000)
logger.info("Секция лицевой стороны найдена")
back_side_section = self.page.locator(RackLocators.BACK_SIDE_SECTION).first
expect(back_side_section).to_be_visible(timeout=10000)
logger.info("Секция обратной стороны найдена")
# Проверяем заголовки
front_side_title = front_side_section.locator(RackLocators.FRONT_SIDE_TITLE)
expect(front_side_title).to_be_visible(timeout=5000), "Заголовок 'Лицевая сторона' не отображается"
logger.info("Заголовок 'Лицевая сторона' отображается")
back_side_title = back_side_section.locator(RackLocators.BACK_SIDE_TITLE)
expect(back_side_title).to_be_visible(timeout=5000), "Заголовок 'Обратная сторона' не отображается"
logger.info("Заголовок 'Обратная сторона' отображается")
# Проверяем позиции юнитов
unit_positions = self.page.locator(RackLocators.UNIT_POSITIONS)
total_positions = unit_positions.count()
logger.info(f"Всего позиций юнитов: {total_positions}")
assert total_positions > 0, "Не найдено позиций юнитов"
# Детальная проверка лицевой стороны
logger.info("--- Детальная проверка лицевой стороны ---")
self._check_front_side_details(front_side_section)
# Детальная проверка обратной стороны
logger.info("--- Детальная проверка обратной стороны ---")
self._check_back_side_details(back_side_section)
logger.info("Все проверки сторон стойки пройдены успешно")
def _check_front_side_details(self, front_side_section) -> None:
"""
Проверка структуры лицевой стороны стойки.
Args:
front_side_section: Локатор секции лицевой стороны
Raises:
AssertionError: Если структура лицевой стороны некорректна
"""
# Проверяем юниты в секции лицевой стороны
front_side_units = front_side_section.locator(RackLocators.FRONT_SIDE_UNITS)
unit_count = front_side_units.count()
logger.info(f"Найдено юнитов на лицевой стороне: {unit_count}")
assert unit_count >= 1, f"Не найдено юнитов на лицевой стороне. Ожидалось минимум 1, найдено {unit_count}"
# Проверяем наличие устройств на лицевой стороне
front_side_devices = front_side_section.locator(RackLocators.FRONT_SIDE_DEVICES)
device_count = front_side_devices.count()
logger.info(f"Найдено физических устройств на лицевой стороне: {device_count}")
if device_count > 0:
for i in range(device_count):
device = front_side_devices.nth(i)
device_title = device.get_attribute("title")
device_classes = device.get_attribute("class") or ""
logger.info(f" Устройство {i}: title='{device_title}', classes='{device_classes}'")
def _check_back_side_details(self, back_side_section) -> None:
"""
Проверка структуры обратной стороны стойки.
Args:
back_side_section: Локатор секции обратной стороны
Raises:
AssertionError: Если структура обратной стороны некорректна
"""
# Проверяем юниты в секции обратной стороны
back_side_units = back_side_section.locator(RackLocators.BACK_SIDE_UNITS)
unit_count = back_side_units.count()
logger.info(f"Найдено юнитов на обратной стороне: {unit_count}")
assert unit_count >= 1, f"Не найдено юнитов на обратной стороне. Ожидалось минимум 1, найдено {unit_count}"
# Проверяем наличие устройств на обратной стороне
back_side_devices = back_side_section.locator(RackLocators.BACK_SIDE_DEVICES)
device_count = back_side_devices.count()
logger.info(f"Найдено физических устройств на обратной стороне: {device_count}")
if device_count > 0:
for i in range(device_count):
device = back_side_devices.nth(i)
device_title = device.get_attribute("title")
device_classes = device.get_attribute("class") or ""
logger.info(f" Устройство {i}: title='{device_title}', classes='{device_classes}'")
def should_be_header_panel(self, expected_toolbar_title_items: list[str]) -> None:
"""
Проверяет наличие и корректность заголовка панели.
Args:
expected_toolbar_title_items: Ожидаемые элементы заголовка
Raises:
AssertionError: Если заголовок панели не соответствует ожиданиям
"""
panel_header_locator = self.page.locator(PANEL_HEADER)
expect(panel_header_locator).to_be_visible(), "Panel header 'Объекты'"
if panel_header_locator.inner_text() != 'chevron_right':
assert False, "No separator 'chevron_right' after header 'Объекты'"
actual_toolbar_title_items = self.get_toolbar_title()
self.check_lists_equals(actual_toolbar_title_items,
expected_toolbar_title_items,
f"Miscomparison actual {actual_toolbar_title_items} and expected {expected_toolbar_title_items}")
self.toolbar.check_button_visibility("edit")
def check_tab_switching(self) -> None:
"""
Проверяет переключение между вкладками стойки в соответствии с локаторами.
Raises:
AssertionError: Если переключение на одну или более вкладок не удалось
"""
logger.info("Тестирование функциональности переключения вкладок стойки...")
# Вкладки
defined_tabs = [
"Общая информация",
"Обслуживание",
"События",
"Сервисы"
]
logger.info(f"Тестируемые определенные вкладки: {defined_tabs}")
successful_switches = 0
failed_switches = []
# Тестируем переключение на каждую определенную вкладку
for tab_name in defined_tabs:
logger.info(f"Тестирование переключения на вкладку '{tab_name}'...")
# Проверяем существование локатора для этой вкладки
tab_locator = RackLocators.TAB_BY_NAME.format(tab_name)
tab_elements = self.page.locator(tab_locator)
# Проверяем наличие элементов через count()
if tab_elements.count() == 0:
logger.warning(f"Вкладка '{tab_name}' не найдена на странице")
failed_switches.append(f"Вкладка '{tab_name}' не найдена")
continue
# Находим видимую и доступную вкладку
target_tab = None
for i in range(tab_elements.count()):
element = tab_elements.nth(i)
# Проверки видимости и доступности
if element.is_visible() and element.is_enabled():
target_tab = element
break
if not target_tab:
logger.warning(f"Не найдена видимая/доступная вкладка '{tab_name}'")
failed_switches.append(f"Вкладка '{tab_name}' не видима/не доступна")
continue
# Переключаемся на вкладку
logger.info(f"Переключение на вкладку '{tab_name}'...")
# Проверяем активность ДО клика
if self.is_tab_active(tab_name):
logger.info(f"Вкладка '{tab_name}' уже активна")
successful_switches += 1
continue
# Кликаем на вкладку с таймаутом
target_tab.click(timeout=5000)
# Ждем изменения активной вкладки
self._wait_for_tab_activation(tab_name)
# Проверяем, что вкладка активна
if not self.is_tab_active(tab_name):
logger.warning(f"Вкладка '{tab_name}' не активна после переключения")
failed_switches.append(f"Вкладка '{tab_name}' не активна после клика")
continue
logger.info(f"Успешно переключено на вкладку '{tab_name}'")
successful_switches += 1
# Небольшая пауза между переключениями для стабильности
self.page.wait_for_timeout(1000)
# Формируем итоговый отчет
logger.info("=== РЕЗУЛЬТАТЫ ПЕРЕКЛЮЧЕНИЯ ВКЛАДОК ===")
logger.info(f"Успешных переключений: {successful_switches}/{len(defined_tabs)}")
if failed_switches:
logger.info("Неудачные переключения:")
for failure in failed_switches:
logger.info(f" - {failure}")
# Требуем успешного переключения на все определенные вкладки
if successful_switches < len(defined_tabs):
raise AssertionError(
f"Тест переключения вкладок не пройден. "
f"Только {successful_switches} из {len(defined_tabs)} определенных вкладок переключены успешно. "
f"Ошибки: {', '.join(failed_switches)}"
)
logger.info(f"Все {successful_switches} определенных вкладок успешно переключены!")

View File

@ -1,609 +0,0 @@
"""Тест создания дочернего элемента 'Стойка'."""
import pytest
from playwright.sync_api import Page
from tools.logger import get_logger
from locators.navigation_panel_locators import NavigationPanelLocators
from locators.rack_locators import RackLocators
from components_derived.accounting_objects.rack_maker import RackObjectMaker, RackData
from components_derived.frames.create_child_element_frame import CreateChildElementFrame
from pages.location_page import LocationPage
from components_derived.modal_edit_rack import ModalEditRack, RackEditData
from pages.login_page import LoginPage
from pages.main_page import MainPage
from pages.rack_page import RackPage
from components.alert_component import AlertComponent
logger = get_logger("CREATE_RACK_TEST")
logger.setLevel("INFO")
# @pytest.mark.smoke
class TestCreateRackElement:
"""Тест создания дочернего элемента типа 'Стойка'.
Тесты покрывают следующие сценарии:
1. test_create_rack_content: Проверяет содержимое формы создания стойки
2. test_create_rack_child_element: Проверяет создание дочернего элемента типа 'Стойка'
3. test_create_rack_with_duplicate_name: Проверяет создание стойки с дублирующимся именем
4. test_required_fields_validation: Проверяет валидацию обязательных полей при создании стойки
"""
# Инициализируем атрибуты
main_page: MainPage = None
location_page: LocationPage = None
@pytest.fixture(scope="function", autouse=True)
def setup(self, browser: Page) -> None:
"""Фикстура для подготовки тестового окружения.
Args:
browser (Page): Экземпляр страницы Playwright для взаимодействия с UI
"""
# Авторизация в системе
login_page = LoginPage(browser)
login_page.do_login()
# Мы на главной странице
self.main_page = MainPage(browser)
self.main_page.should_be_navigation_panel()
# Переходим к Объектам
self.main_page.click_main_navigation_panel_item("Объекты")
self.main_page.wait_for_timeout(1000)
self.main_page.click_main_navigation_panel_item("test-zone")
# Создаем экземпляр страницы локации
self.location_page = LocationPage(browser)
@pytest.fixture
def cleanup_racks(self, browser: Page):
"""Фикстура для очистки созданных стоек."""
# Список для хранения созданных в тесте стоек
created_racks = []
yield created_racks
# После завершения теста удаляем созданные стойки
if created_racks:
logger.debug(f"Cleaning up racks: {created_racks}")
self.main_page.wait_for_timeout(500)
self.main_page.click_subpanel_item("test-zone")
self.main_page.wait_for_timeout(1000)
# Удаляем каждую стойку если она существует
for rack_name in created_racks:
# Проверяем существование стойки
if self._check_rack_existance(browser, rack_name):
logger.debug(f"Deleting rack '{rack_name}'...")
# Переходим на страницу стойки для удаления
self.main_page.click_subpanel_item(rack_name, parent="test-zone")
self.main_page.wait_for_timeout(1000)
# Удаляем стойку
self._delete_rack_from_context_menu(browser, rack_name)
# Проверяем удаление
self.main_page.click_subpanel_item("test-zone")
self.main_page.wait_for_timeout(500)
# Дополнительная проверка удаления
rack_still_exists = self._check_rack_existance(browser, rack_name)
if rack_still_exists:
logger.error(f"Rack '{rack_name}' still exists after deletion attempt")
logger.debug("Racks cleanup completed")
else:
logger.debug("No racks to cleanup")
def _create_rack(self, browser: Page, rack_name: str) -> None:
"""Создает стойку.
Args:
browser: Страница Playwright
rack_name: Имя стойки для создания
"""
logger.debug(f"Creating rack with name '{rack_name}'")
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Создаем фрейм создания дочернего элемента
create_child_frame = CreateChildElementFrame(browser)
# Нажимаем на плашку "Класс объекта учета"
create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
create_child_frame.select_object_class("Стойка")
# Открывается набор плашек для задания параметров стойки
rack_maker = RackObjectMaker(browser)
# Создаем объект данных стойки
rack_data = RackData(
name=rack_name,
height="42",
depth="1000"
)
# Заполняем данные стойки
rack_maker.fill_rack_data(rack_data)
# Нажимаем кнопку создания
create_child_frame.click_add_button()
# Проверяем уведомление об успешном создании
alert = AlertComponent(browser)
expected_alert_text = f"Элемент {rack_name} создан"
alert.check_alert_presence(expected_alert_text)
alert.close_alert_by_text(expected_alert_text)
def _delete_rack_from_context_menu(self, browser: Page, rack_name: str) -> None:
"""Удаляет стойку через контекстное меню.
Args:
browser: Страница Playwright
rack_name: Имя стойки для удаления
"""
logger.debug(f"Deleting rack '{rack_name}' from context menu...")
# 1. Находим элемент стойки в навигационной панели
rack_element = browser.locator(NavigationPanelLocators.TREEVIEW).get_by_text(rack_name, exact=True).first
# Прокручиваем до элемента если нужно
rack_element.scroll_into_view_if_needed()
self.main_page.wait_for_timeout(500)
# 2. Проверяем и нажимаем кнопку "Изменить"
rack_page = RackPage(browser)
# Проверяем видимость и тултип кнопки
rack_page.should_be_toolbar_buttons()
# Кликаем на кнопку "Изменить"
rack_page.click_edit_button()
self.main_page.wait_for_timeout(1000)
# 3. Создаем экземпляр ModalRackEditRack
rack_edit = ModalEditRack(browser, rack_name)
# Используем метод для удаления
rack_edit.click_remove_button()
self.main_page.wait_for_timeout(1000)
# 4. Проверяем уведомление об успешном удалении
alert = AlertComponent(browser)
expected_alert_text = "Успешно удалено"
alert.check_alert_presence(expected_alert_text)
# Получаем текст alert для логирования
alert_text = alert.get_text()
logger.debug(f"Alert text after deletion: {alert_text}")
# Закрываем alert
alert.close_alert_by_text(expected_alert_text)
logger.debug("Rack deletion completed")
def _perform_required_fields_test(self, create_child_frame, rack_maker, test_data):
"""Выполняет один тест валидации обязательных полей.
Args:
create_child_frame: Фрейм создания дочернего элемента
rack_maker: Объект для работы со стойкой
test_data: Словарь с данными теста
"""
# Распаковываем данные теста
name_value = test_data["name"]
height_value = test_data["height"]
depth_value = test_data["depth"]
expected_alert_height = test_data["expected_alert_height"]
expected_alert_depth = test_data["expected_alert_depth"]
# Получаем контейнер формы
container_locator = create_child_frame.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1)
logger.debug(f"Available fields:\
{list(create_child_frame.get_input_fields_locators(container_locator).keys())}")
# Проверяем и очищаем поле "Глубина (мм)" только если оно заполнено
logger.debug("Checking field: Глубина (мм)")
if create_child_frame.is_field_filled("Глубина (мм)", container_locator):
logger.debug("Field 'Глубина (мм)' is filled, performing clearing")
create_child_frame.clear_combobox_field("Глубина (мм)")
logger.debug("Clearing completed for 'Глубина (мм)'")
else:
logger.debug("Field 'Глубина (мм)' is already empty, skipping clearing")
# Проверяем и очищаем поле "Высота в юнитах" только если оно заполнено
logger.debug("Checking field: Высота в юнитах")
if create_child_frame.is_field_filled("Высота в юнитах", container_locator):
logger.debug("Field 'Высота в юнитах' is filled, performing clearing")
create_child_frame.clear_combobox_field("Высота в юнитах")
logger.debug("Clearing completed for 'Высота в юнитах'")
else:
logger.debug("Field 'Высота в юнитах' is already empty, skipping clearing")
# Создаем объект данных стойки
rack_data = RackData(
name=name_value,
height=height_value,
depth=depth_value
)
# Заполняем данные стойки
logger.debug(f"Setting test data - Name: '{name_value}', Height: '{height_value}', Depth: '{depth_value}'")
rack_maker.fill_rack_data(rack_data)
# Нажимаем кнопку создания
logger.debug("Submitting form for validation")
create_child_frame.click_add_button()
create_child_frame.wait_for_timeout(500)
# Проверяем валидацию полей
logger.debug("Checking validation results")
alert = AlertComponent(create_child_frame.page)
# Обрабатываем alert-окна
if not height_value:
logger.debug("Expecting height validation alert")
alert.check_alert_presence(expected_alert_height)
alert.close_alert_by_text(expected_alert_height)
logger.debug("Height alert handled")
if not depth_value:
logger.debug("Expecting depth validation alert")
alert.check_alert_presence(expected_alert_depth)
alert.close_alert_by_text(expected_alert_depth)
logger.debug("Depth alert handled")
# Проверяем подсветку обязательных полей
if height_value:
create_child_frame.check_field_error_not_highlighted("Высота в юнитах")
logger.debug("Height field validation passed")
else:
create_child_frame.check_field_error_highlighted("Высота в юнитах")
logger.debug("Height field validation failed as expected")
if depth_value:
create_child_frame.check_field_error_not_highlighted("Глубина (мм)")
logger.debug("Depth field validation passed")
else:
create_child_frame.check_field_error_highlighted("Глубина (мм)")
logger.debug("Depth field validation failed as expected")
# Проверяем, что остались на странице создания
create_child_frame.check_toolbar_title('Создать дочерний элемент в')
logger.debug("Test completed successfully")
def test_create_rack_child_element(self, browser: Page, cleanup_racks) -> None:
"""Тест создания дочернего элемента типа 'Стойка'."""
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Создаем фрейм создания дочернего элемента
create_child_frame = CreateChildElementFrame(browser)
# Нажимаем на плашку "Класс объекта учета"
create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
create_child_frame.select_object_class("Стойка")
# Открывается набор плашек для задания параметров стойки
rack_maker = RackObjectMaker(browser)
# Создаем объект данных стойки
rack_data = RackData(
name="Test-Rack-01",
height="42",
depth="1000",
serial="TEST123456",
inventory="INV-001",
comment="Тестовая стойка для автоматизации",
state="Введен в эксплуатацию",
cable_entry="сверху"
)
# Сохраняем имя стойки в переменную
rack_name = rack_data.name
cleanup_racks.append(rack_name)
# Заполняем данные стойки
rack_maker.fill_rack_data(rack_data)
# Нажимаем кнопку "Добавить"
create_child_frame.click_add_button()
# Проверяем уведомление об успешном создании стойки
alert = AlertComponent(browser)
expected_alert_text = f"Элемент {rack_name} создан"
alert.check_alert_presence(expected_alert_text)
# Закрываем alert
alert.close_alert_by_text(expected_alert_text)
# Проверяем, что стойка создана и отображается
logger.debug(f"Verifying that rack '{rack_name}' was created...")
# Обновляем навигационную панель
self.main_page.click_main_navigation_panel_item("test-zone")
# Проверяем существование стойки в навигационной панели
rack_exists = self._check_rack_existance(browser, rack_name)
assert rack_exists, f"Rack '{rack_name}' should be visible in navigation panel after creation"
logger.debug(f"Rack '{rack_name}' is visible in navigation panel")
logger.debug("Test for creating 'Rack' child element completed successfully")
def test_create_rack_content(self, browser: Page) -> None:
"""Тест проверки содержимого формы создания стойки."""
# Проверяем что кнопка "Создать" доступна
self.location_page.should_be_toolbar_buttons()
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Создаем фрейм создания дочернего элемента
create_child_frame = CreateChildElementFrame(browser)
# Нажимаем на плашку "Класс объекта учета"
create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
create_child_frame.select_object_class("Стойка")
# Открывается набор плашек для задания параметров стойки
rack_maker = RackObjectMaker(browser)
# Проверяем заголовок формы создания
create_child_frame.check_toolbar_title('Создать дочерний элемент в')
# Проверяем что после выбора 'Стойка' появляются специфичные поля
rack_maker.check_rack_fields_presence()
logger.debug("Rack-specific fields are displayed correctly")
create_child_frame.should_be_toolbar_buttons()
def test_create_rack_with_duplicate_name(self, browser: Page, cleanup_racks) -> None:
"""Тест создания стойки с уже существующим именем.
Проверяет, что система корректно обрабатывает попытку создания
стойки с именем, которое уже используется.
"""
logger.debug("Starting test for creating rack with duplicate name")
rack_name = "Test-Rack-Duplicate"
# Проверяем, существует ли уже стойка с таким именем
if not self._check_rack_existance(browser, rack_name):
logger.debug(f"Rack with name '{rack_name}' not found. Creating first rack.")
self._create_rack(browser, rack_name)
logger.debug(f"First rack with name '{rack_name}' created successfully")
# Добавляем стойку в список для очистки
cleanup_racks.append(rack_name)
else:
logger.debug(f"Rack with name '{rack_name}' already exists, proceeding to create second one")
# Создаем вторую стойку с тем же именем
logger.debug(f"Attempting to create second rack with name '{rack_name}'")
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Создаем фрейм создания дочернего элемента
create_child_frame = CreateChildElementFrame(browser)
# Нажимаем на плашку "Класс объекта учета"
create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
create_child_frame.select_object_class("Стойка")
# Открывается набор плашек для задания параметров стойки
rack_maker = RackObjectMaker(browser)
# Создаем объект данных для второй стойки
rack_data = RackData(
name=rack_name,
height="42",
depth="450"
)
# Пытаемся создать вторую стойку с тем же именем
rack_maker.fill_rack_data(rack_data)
# Нажимаем кнопку создания
create_child_frame.click_add_button()
create_child_frame.wait_for_timeout(1000)
# Проверяем наличие alert-окна с сообщением о дублирующемся имени
alert = AlertComponent(browser)
expected_alert_text = f"Имя {rack_name} уже используется"
alert.check_alert_presence(expected_alert_text)
# Закрываем alert-окно с помощью кнопки закрытия
create_child_frame.wait_for_timeout(2000)
alert.close_alert_by_text(expected_alert_text)
# Проверяем, что остались на странице создания (стойка не создана)
create_child_frame.check_toolbar_title('Создать дочерний элемент в')
logger.debug("System prevented creating rack with duplicate name")
def test_required_fields_validation(self, browser: Page, cleanup_racks) -> None:
"""Тест проверки обязательных полей при создании стойки.
Проверяет, что система корректно валидирует обязательные поля:
- Поле 'Высота в юнитах' должно быть заполнено
- Поле 'Глубина (мм)' должно быть заполнено
"""
# Текст сообщения alert-окна
expected_alert_text_height = "поле Высота в юнитах должно быть заполнено"
expected_alert_text_depth = "поле Глубина (мм) должно быть заполнено"
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Создаем фрейм создания дочернего элемента
create_child_frame = CreateChildElementFrame(browser)
# Нажимаем на плашку "Класс объекта учета"
create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
create_child_frame.select_object_class("Стойка")
# Открывается набор плашек для задания параметров стойки
rack_maker = RackObjectMaker(browser)
# Тестовые данные
test_cases = [
{
"name": "Test 1: Required fields are not filled",
"data": {
"name": "Test-Rack-Required-01",
"height": "",
"depth": "",
"expected_alert_height": expected_alert_text_height,
"expected_alert_depth": expected_alert_text_depth
}
},
{
"name": "Test 2: Only 'Height in units' field is filled",
"data": {
"name": "Test-Rack-Required-02",
"height": "42",
"depth": "",
"expected_alert_height": expected_alert_text_height,
"expected_alert_depth": expected_alert_text_depth
}
},
{
"name": "Test 3: Only 'Depth (mm)' field is filled",
"data": {
"name": "Test-Rack-Required-03",
"height": "",
"depth": "1000",
"expected_alert_height": expected_alert_text_height,
"expected_alert_depth": expected_alert_text_depth
}
}
]
# Выполняем тестовые случаи
for test_case in test_cases:
logger.debug(test_case["name"])
self._perform_required_fields_test(
create_child_frame, rack_maker, test_case["data"])
logger.debug("System prevented creating rack with invalid required fields")
# 4. Тест: Заполняем все обязательные поля
logger.debug("Test 4: All required fields are filled")
# Генерируем уникальное имя для финального теста
final_rack_name = "Test-Rack-Required-04"
cleanup_racks.append(final_rack_name)
# **ВАЖНО: Очищаем поля перед заполнением**
logger.debug("Clearing fields before filling...")
# Получаем контейнер формы
container_locator = create_child_frame.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1)
fields_locators = create_child_frame.get_input_fields_locators(container_locator)
# Очищаем поле "Высота в юнитах" если оно заполнено
if "Высота в юнитах" in fields_locators:
if create_child_frame.is_field_filled("Высота в юнитах", container_locator):
logger.debug("Clearing 'Высота в юнитах' field...")
create_child_frame.clear_combobox_field("Высота в юнитах")
create_child_frame.wait_for_timeout(500)
# Очищаем поле "Глубина (мм)" если оно заполнено
if "Глубина (мм)" in fields_locators:
if create_child_frame.is_field_filled("Глубина (мм)", container_locator):
logger.debug("Clearing 'Глубина (мм)' field...")
create_child_frame.clear_combobox_field("Глубина (мм)")
create_child_frame.wait_for_timeout(500)
# Очищаем поле "Имя" если оно заполнено
if "Имя" in fields_locators:
if create_child_frame.is_field_filled("Имя", container_locator):
logger.debug("Clearing 'Имя' field...")
# Специальная обработка для текстового поля
field_container = fields_locators["Имя"]
input_field = field_container.locator("input").first
if input_field.count() > 0:
input_field.click()
input_field.press("Control+A")
input_field.press("Backspace")
create_child_frame.wait_for_timeout(500)
# Создаем объект данных стойки
rack_data = RackData(
name=final_rack_name,
height="42",
depth="1000"
)
# Заполняем все обязательные поля
rack_maker.fill_rack_data(rack_data)
# Проверяем, что ни одно поле не подсвечено цветом ошибки
create_child_frame.check_field_error_not_highlighted("Имя")
create_child_frame.check_field_error_not_highlighted("Высота в юнитах")
create_child_frame.check_field_error_not_highlighted("Глубина (мм)")
logger.debug("No required fields are highlighted with error color - all fields filled correctly")
# Нажимаем кнопку создания
create_child_frame.click_add_button()
create_child_frame.wait_for_timeout(500)
# Проверяем уведомление об успешном создании стойки
alert = AlertComponent(browser)
expected_alert_text = f"Элемент {final_rack_name} создан"
alert.check_alert_presence(expected_alert_text)
# Закрываем alert
alert.close_alert_by_text(expected_alert_text)
logger.debug("Required fields validation test completed successfully")
# Вспомогательные методы проверки
def _check_rack_existance(self, browser: Page, rack_name: str) -> bool:
"""Проверяет существование стойки.
Args:
browser: Страница Playwright
rack_name: Имя стойки для проверки
Returns:
bool: True если стойка существует, False в противном случае
"""
logger.debug(f"Checking existence of rack with name '{rack_name}'")
# Обновляем навигационную панель
self.main_page.click_main_navigation_panel_item("Объекты")
self.main_page.click_main_navigation_panel_item("Объекты")
self.main_page.click_subpanel_item("test-zone")
nav_panel_locator = NavigationPanelLocators.TREEVIEW
# Проверяем видимость элемента
element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first
if element.is_visible():
logger.debug(f"Rack with name '{rack_name}' found")
return True
logger.debug(f"Rack with name '{rack_name}' not found")
return False

View File

@ -0,0 +1,474 @@
"""Тест создания дочернего элемента 'Стойка'."""
import pytest
from playwright.sync_api import Page
from tools.logger import get_logger
from locators.navigation_panel_locators import NavigationPanelLocators
from frames.create_element_frame import CreateElementFrame
from forms.create_rack_form import CreateRackForm, CreateRackData
from pages.location_page import LocationPage
from makers.edit_rack_maker import EditRackMaker
from pages.login_page import LoginPage
from pages.main_page import MainPage
from pages.rack_page import RackPage
from components.alert_component import AlertComponent
logger = get_logger("CREATE_RACK_TEST")
logger.setLevel("INFO")
class TestCreateRack:
"""Тест создания дочернего элемента типа 'Стойка'."""
# Единое имя для тестовой стойки
TEST_RACK_NAME = "Test-Rack-Create"
# Для теста с дубликатом используем отдельное имя
DUPLICATE_RACK_NAME = "Test-Rack-Duplicate"
# Инициализируем атрибуты
main_page: MainPage = None
location_page: LocationPage = None
alert: AlertComponent = None
create_child_frame: CreateElementFrame = None
rack_form: CreateRackForm = None
@pytest.fixture(scope="function", autouse=True)
def setup(self, browser: Page) -> None:
"""Фикстура для подготовки тестового окружения.
Args:
browser: Экземпляр страницы Playwright для взаимодействия с UI
"""
# Авторизация в системе
login_page = LoginPage(browser)
login_page.do_login()
# Мы на главной странице
self.main_page = MainPage(browser)
self.main_page.should_be_navigation_panel()
# Переходим к Объектам
self.main_page.click_main_navigation_panel_item("Объекты")
self.main_page.wait_for_timeout(1000)
self.main_page.click_main_navigation_panel_item("test-zone")
# Создаем экземпляр страницы локации
self.location_page = LocationPage(browser)
# Инициализируем компонент алертов
self.alert = AlertComponent(browser)
# Инициализируем фрейм создания дочернего элемента
self.create_child_frame = CreateElementFrame(browser)
# Инициализируем форму создания Стойки
self.rack_form = CreateRackForm(browser)
@pytest.fixture
def cleanup_racks(self, browser: Page):
"""Фикстура для очистки созданных стоек."""
created_racks = []
yield created_racks
# После завершения теста удаляем созданные стойки
if created_racks:
logger.debug(f"Cleaning up racks: {created_racks}")
self.main_page.wait_for_timeout(500)
self.main_page.click_subpanel_item("test-zone")
self.main_page.wait_for_timeout(1000)
for rack_name in created_racks:
if self._check_rack_existance(browser, rack_name):
logger.debug(f"Deleting rack '{rack_name}'...")
self.main_page.click_subpanel_item(rack_name, parent="test-zone")
self.main_page.wait_for_timeout(1000)
self._delete_rack(browser, rack_name)
self.main_page.click_subpanel_item("test-zone")
self.main_page.wait_for_timeout(500)
def _create_rack(self, browser: Page, rack_data: CreateRackData) -> None:
"""Создает стойку с использованием унифицированного подхода.
Args:
browser: Страница Playwright
rack_data: Данные стойки для создания
"""
logger.debug(f"Creating rack with name '{rack_data.name}'")
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Нажимаем на плашку "Класс объекта учета"
self.create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
self.create_child_frame.select_object_class("Стойка")
# Создаем форму создания стойки
rack_form = CreateRackForm(browser)
# Заполняем данные стойки
fill_results = rack_form.fill_rack_data(rack_data)
logger.debug(f"Fill results: {fill_results}")
# Нажимаем кнопку создания
self.create_child_frame.click_add_button()
# Ждем появления alert с текстом
expected_alert_text = f"Элемент {rack_data.name} создан"
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
# Если уже закрылся - игнорируем
logger.debug("Alert already closed by the time forcible close was attempted")
logger.info(f"Rack '{rack_data.name}' created successfully")
def _delete_rack(self, browser: Page, rack_name: str) -> None:
"""Удаляет стойку через контекстное меню.
Args:
browser: Страница Playwright
rack_name: Имя стойки для удаления
"""
# Находим элемент стойки в навигационной панели
rack_element = browser.locator(
NavigationPanelLocators.TREEVIEW
).get_by_text(rack_name, exact=True).first
# Прокручиваем до элемента если нужно
rack_element.scroll_into_view_if_needed()
self.main_page.wait_for_timeout(500)
# Проверяем и нажимаем кнопку "Изменить"
rack_page = RackPage(browser)
# Проверяем видимость и тултип кнопки
rack_page.should_be_toolbar_buttons()
# Кликаем на кнопку "Изменить"
rack_page.click_edit_button()
self.main_page.wait_for_timeout(1000)
# Создаем экземпляр EditRackMaker
rack_edit = EditRackMaker(browser, rack_name)
# Используем метод для удаления
rack_edit.click_remove_button()
# Проверяем уведомление об успешном удалении
expected_alert_text = "Успешно удалено"
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
# Если уже закрылся - игнорируем
logger.debug("Alert already closed by the time forcible close was attempted")
logger.info(f"Rack '{rack_name}' deleted successfully")
def _check_rack_existance(self, browser: Page, rack_name: str) -> bool:
"""Проверяет существование стойки.
Args:
browser: Страница Playwright
rack_name: Имя стойки для проверки
Returns:
bool: True если стойка существует, False в противном случае
"""
logger.debug(f"Checking existence of rack with name '{rack_name}'")
self.main_page.click_subpanel_item("test-zone")
nav_panel_locator = NavigationPanelLocators.TREEVIEW
element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first
if element.is_visible():
logger.debug(f"Rack with name '{rack_name}' found")
return True
logger.debug(f"Rack with name '{rack_name}' not found")
return False
def test_create_rack_content(self, browser: Page) -> None:
"""Тест проверки содержимого формы создания стойки."""
# Проверяем что кнопка "Создать" доступна
self.location_page.should_be_toolbar_buttons()
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Проверяем заголовок формы создания
self.create_child_frame.check_toolbar_title('Создать дочерний элемент в')
# Нажимаем на плашку "Класс объекта учета"
self.create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
self.create_child_frame.select_object_class("Стойка")
# Создаем форму создания стойки и проверяем наличие полей
rack_form = CreateRackForm(browser)
# Проверяем, что основные поля присутствуют
assert rack_form.get_content_item("name_input") is not None, "Name field not initialized"
assert rack_form.get_content_item("usize_input") is not None, "Height field not initialized"
assert rack_form.get_content_item("depth_input") is not None, "Depth field not initialized"
logger.debug("Rack-specific fields are displayed correctly")
self.create_child_frame.should_be_toolbar_buttons()
def test_create_rack(self, browser: Page, cleanup_racks) -> None:
"""Тест создания дочернего элемента типа 'Стойка'."""
logger.debug(f"Starting test with rack name: {self.TEST_RACK_NAME}")
# Создаем данные стойки с расширенным набором полей
rack_data = CreateRackData(
name=self.TEST_RACK_NAME,
usize="42",
depth="1000",
serial="TEST123456",
inventory="INV-001",
comment="Тестовая стойка для автоматизации",
cable_entry="сверху",
state="Введен в эксплуатацию",
# owner="Владелец",
# service_org="Обслуживающая организация",
# project="Проект/Титул"
)
# Сохраняем имя стойки для очистки
cleanup_racks.append(rack_data.name)
# Проверяем, существует ли уже стойка с таким именем
if self._check_rack_existance(browser, rack_data.name):
logger.warning(f"Rack '{rack_data.name}' already exists. Deleting it before creating new one...")
# Переходим к стойке для удаления
self.main_page.click_subpanel_item(rack_data.name, parent="test-zone")
self.main_page.wait_for_timeout(1000)
# Удаляем существующую стойку
self._delete_rack(browser, rack_data.name)
logger.debug(f"Existing rack '{rack_data.name}' deleted successfully")
# Создаем новую стойку
self._create_rack(browser, rack_data)
# Проверяем, что стойка создана и отображается
logger.debug(f"Verifying that rack '{rack_data.name}' was created...")
self.main_page.click_main_navigation_panel_item("test-zone")
rack_exists = self._check_rack_existance(browser, rack_data.name)
assert rack_exists, f"Rack '{rack_data.name}' should be visible in navigation panel after creation"
logger.debug(f"Rack '{rack_data.name}' is visible in navigation panel")
logger.debug("Test for creating 'Rack' child element completed successfully")
def test_create_rack_with_duplicate_name(self, browser: Page, cleanup_racks) -> None:
"""Тест создания стойки с уже существующим именем."""
logger.debug(f"Starting test for creating rack with duplicate name: {self.DUPLICATE_RACK_NAME}")
rack_name = self.DUPLICATE_RACK_NAME
# Создаем первую стойку если её нет
if not self._check_rack_existance(browser, rack_name):
logger.debug(f"Creating first rack with name '{rack_name}'")
first_rack_data = CreateRackData(
name=rack_name,
usize="42",
depth="1000"
)
self._create_rack(browser, first_rack_data)
cleanup_racks.append(rack_name)
# Пытаемся создать вторую стойку с тем же именем
logger.debug(f"Attempting to create second rack with name '{rack_name}'")
self.location_page.click_create_button()
# Нажимаем на плашку "Класс объекта учета"
self.create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
self.create_child_frame.select_object_class("Стойка")
rack_form = CreateRackForm(browser)
duplicate_rack_data = CreateRackData(
name=rack_name,
usize="42",
depth="450"
)
self.create_child_frame.check_toolbar_title('Создать дочерний элемент в')
rack_form.fill_rack_data(duplicate_rack_data)
self.create_child_frame.click_add_button()
expected_alert_text = f"Имя {rack_name} уже используется"
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
# Если уже закрылся - игнорируем
logger.debug("Alert already closed by the time forcible close was attempted")
logger.debug("System prevented creating rack with duplicate name")
def test_required_fields_validation(self, browser: Page, cleanup_racks) -> None:
"""Тест проверки обязательных полей при создании стойки."""
logger.debug("Starting required fields validation test")
expected_alert_text_height = "поле Высота в юнитах должно быть заполнено"
expected_alert_text_depth = "поле Глубина (мм) должно быть заполнено"
expected_alert_text_name = "Поле Имя должно быть установлено"
self.main_page.click_main_navigation_panel_item("test-zone")
# Открываем форму создания
self.location_page.click_create_button()
# Нажимаем на плашку "Класс объекта учета"
self.create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
self.create_child_frame.select_object_class("Стойка")
rack_form = CreateRackForm(browser)
# ========== Тест 1: Обязательные поля высота и глубина пустые ==========
logger.debug("Test 1: Both required fields (height, depth) are empty")
# Очищаем поля высоты и глубины перед заполнением
rack_form.clear_field("Высота в юнитах")
rack_form.clear_field("Глубина (мм)")
test_data_1 = CreateRackData(
name=self.TEST_RACK_NAME,
usize="",
depth=""
)
rack_form.fill_rack_data(test_data_1)
self.create_child_frame.click_add_button()
self.create_child_frame.wait_for_timeout(500)
# Проверяем alert для высоты, глубины
self.alert.check_alert_presence(expected_alert_text_height, timeout=5000)
self.alert.check_alert_presence(expected_alert_text_depth, timeout=5000)
# Проверяем, закрылся ли автоматически alert для высоты, глубины
self.alert.check_alert_absence(expected_alert_text_height, timeout=7000)
self.alert.check_alert_absence(expected_alert_text_depth, timeout=500)
# Проверяем подсветку полей
field_status = rack_form.verify_required_fields_highlighted(["Высота в юнитах", "Глубина (мм)"])
logger.debug(f"Field status after test 1: {field_status}")
assert field_status.get("Высота в юнитах"), f"Height field should be highlighted, got: {field_status}"
assert field_status.get("Глубина (мм)"), f"Depth field should be highlighted, got: {field_status}"
# ========== Тест 2: Только высота заполнена ==========
logger.debug("Test 2: Only height field is filled")
# Очищаем поле глубины перед новым заполнением
rack_form.clear_field("Глубина (мм)")
test_data_2 = CreateRackData(
name=self.TEST_RACK_NAME,
usize="42",
depth=""
)
rack_form.fill_rack_data(test_data_2)
self.create_child_frame.click_add_button()
# Проверяем alert для глубины
self.alert.check_alert_presence(expected_alert_text_depth, timeout=5000)
# Проверяем, закрылся ли автоматически alert для глубины
self.alert.check_alert_absence(expected_alert_text_depth, timeout=7000)
# Проверяем подсветку полей
field_status = rack_form.verify_required_fields_highlighted(["Глубина (мм)"])
logger.debug(f"Field status after test 2: {field_status}")
assert field_status.get("Глубина (мм)"), f"Depth field should be highlighted, got: {field_status}"
# ========== Тест 3: Только глубина заполнена ==========
logger.debug("Test 3: Only depth field is filled")
# Очищаем поле высоты перед новым заполнением
rack_form.clear_field("Высота в юнитах")
test_data_3 = CreateRackData(
name=self.TEST_RACK_NAME,
usize="",
depth="1000"
)
rack_form.fill_rack_data(test_data_3)
self.create_child_frame.click_add_button()
# Проверяем alert для высоты
self.alert.check_alert_presence(expected_alert_text_height, timeout=5000)
# Проверяем, закрылся ли автоматически alert для высоты
self.alert.check_alert_absence(expected_alert_text_height, timeout=7000)
# Проверяем подсветку полей
field_status = rack_form.verify_required_fields_highlighted(["Высота в юнитах"])
logger.debug(f"Field status after test 3: {field_status}")
assert field_status.get("Высота в юнитах"), f"Height field should be highlighted, got: {field_status}"
# ========== Тест 4: Поле "Имя" не заполнено ==========
logger.debug("Test 4: Name field is empty")
# Очищаем поле имени
rack_form.clear_field("Имя")
test_data_4 = CreateRackData(
name="",
usize="42",
depth="1000"
)
rack_form.fill_rack_data(test_data_4)
self.create_child_frame.click_add_button()
self.create_child_frame.wait_for_timeout(500)
# Проверяем alert для имени
self.alert.check_alert_presence(expected_alert_text_name, timeout=5000)
# Проверяем, закрылся ли автоматически alert для высоты
self.alert.check_alert_absence(expected_alert_text_name, timeout=7000)
logger.debug("Test 4 completed: System correctly validates empty name field")

View File

@ -1,157 +1,45 @@
"""Модуль тестов вкладки 'Стойка' в модуле Объекты. """Модуль тестов редактирования стойки в модуле Объекты.
Содержит тесты для проверки функциональности Содержит тесты для проверки функциональности
работы со стойкой оборудования. редактирования стойки оборудования.
""" """
import os import os
import pytest import pytest
from playwright.sync_api import Page from playwright.sync_api import Page
from tools.logger import get_logger
from locators.navigation_panel_locators import NavigationPanelLocators from locators.navigation_panel_locators import NavigationPanelLocators
from frames.create_element_frame import CreateElementFrame
from forms.create_rack_form import CreateRackForm, CreateRackData
from makers.edit_rack_maker import EditRackMaker, EditRackData
from pages.location_page import LocationPage
from pages.login_page import LoginPage from pages.login_page import LoginPage
from pages.main_page import MainPage from pages.main_page import MainPage
from pages.location_page import LocationPage
from pages.rack_page import RackPage from pages.rack_page import RackPage
from components_derived.accounting_objects.rack_maker import RackObjectMaker, RackData
from components_derived.frames.create_child_element_frame import CreateChildElementFrame
from components_derived.modal_edit_rack import ModalEditRack, RackEditData
from components.alert_component import AlertComponent from components.alert_component import AlertComponent
from tools.logger import get_logger
# Константы
RACK_NAME = "Test-Rack-Functionality"
# Инициализация логгера для всего модуля
logger = get_logger("RACK_EDIT_TESTS") logger = get_logger("RACK_EDIT_TESTS")
logger.setLevel("INFO") logger.setLevel("INFO")
class TestRackTab:
"""Набор тестов для вкладки 'Стойка' в модуле Объекты.
Проверяет корректность отображения, функциональность элементов интерфейса class TestRackEdit:
и переключение между вкладками стойки оборудования. """Набор тестов для редактирования стойки в модуле Объекты.
Тесты покрывают следующие функциональные области: Проверяет функциональность редактирования различных вкладок стойки:
1. test_rack_general_info_tab_fields - Заполнение полей вкладки 'Общая информация' 1. Общая информация
2. test_rack_image_tab - Работа с вкладкой 'Изображение' 2. Изображение
3. test_rack_access_rules - Заполнение полей правил доступа 3. Правила доступа
""" """
# Имя тестовой стойки
RACK_NAME = "Test-Rack-Edit"
# Инициализируем атрибуты # Инициализируем атрибуты
main_page: MainPage = None main_page: MainPage = None
location_page: LocationPage = None location_page: LocationPage = None
alert: AlertComponent = None
def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: create_child_frame: CreateElementFrame = None
"""Проверяет существование стойки.
Args:
browser: Страница Playwright
rack_name: Имя стойки для проверки
Returns:
bool: True если стойка существует, False в противном случае
"""
# Обновляем навигационную панель
self.main_page.wait_for_timeout(500)
self.main_page.click_subpanel_item("test-zone")
nav_panel_locator = NavigationPanelLocators.TREEVIEW
# Проверяем видимость элемента
element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first
if element.is_visible():
return True
return False
def _create_rack(self, browser: Page, rack_name: str) -> None:
"""Создает стойку.
Args:
browser: Страница Playwright
rack_name: Имя стойки для создания
"""
logger.debug(f"Creating rack: {rack_name}")
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Создаем фрейм создания дочернего элемента
create_child_frame = CreateChildElementFrame(browser)
# Нажимаем на плашку "Класс объекта учета"
create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
create_child_frame.select_object_class("Стойка")
# Открывается набор плашек для задания параметров стойки
rack_maker = RackObjectMaker(browser)
# Создаем объект данных стойки
rack_data = RackData(
name=rack_name,
height="42",
depth="1000"
)
# Заполняем данные стойки
rack_maker.fill_rack_data(rack_data)
# Нажимаем кнопку создания
create_child_frame.click_add_button()
# Проверяем уведомление об успешном создании
alert = AlertComponent(browser)
expected_alert_text = f"Элемент {rack_name} создан"
alert.check_alert_presence(expected_alert_text)
alert.close_alert_by_text(expected_alert_text)
logger.info(f"Rack '{rack_name}' created successfully")
def _delete_rack_from_context_menu(self, browser: Page, rack_name: str) -> None:
"""Удаляет стойку через контекстное меню.
Args:
browser: Страница Playwright
rack_name: Имя стойки для удаления
"""
# 1. Находим элемент стойки в навигационной панели
rack_element = browser.locator(
NavigationPanelLocators.TREEVIEW
).get_by_text(rack_name, exact=True).first
# Прокручиваем до элемента если нужно
rack_element.scroll_into_view_if_needed()
self.main_page.wait_for_timeout(500)
# 2. Проверяем и нажимаем кнопку "Изменить"
rack_page = RackPage(browser)
# Проверяем видимость и тултип кнопки
rack_page.should_be_toolbar_buttons()
# Кликаем на кнопку "Изменить"
rack_page.click_edit_button()
self.main_page.wait_for_timeout(1000)
# 3. Создаем экземпляр ModalRackEditRack
rack_edit = ModalEditRack(browser, rack_name)
# Используем метод для удаления
rack_edit.click_remove_button()
self.main_page.wait_for_timeout(1000)
# 4. Проверяем уведомление об успешном удалении
alert = AlertComponent(browser)
expected_alert_text = "Успешно удалено"
alert.check_alert_presence(expected_alert_text)
alert.close_alert_by_text(expected_alert_text)
logger.info(f"Rack '{rack_name}' deleted successfully")
@pytest.fixture(scope="function", autouse=True) @pytest.fixture(scope="function", autouse=True)
def setup(self, browser: Page) -> None: def setup(self, browser: Page) -> None:
@ -159,11 +47,13 @@ class TestRackTab:
Выполняет: Выполняет:
1. Авторизацию в системе 1. Авторизацию в системе
2. Создание стойки если она не существует 2. Переход к локации test-zone
3. Переход к стойке 3. Инициализацию компонентов
4. Создание стойки если она не существует
5. Переход к стойке
Args: Args:
browser (Page): Экземпляр страницы Playwright для взаимодействия с UI browser: Экземпляр страницы Playwright для взаимодействия с UI
""" """
# Авторизация в системе # Авторизация в системе
@ -182,22 +72,35 @@ class TestRackTab:
# Создаем экземпляр страницы локации # Создаем экземпляр страницы локации
self.location_page = LocationPage(browser) self.location_page = LocationPage(browser)
# Инициализируем компонент алертов (вынесено в атрибуты класса)
self.alert = AlertComponent(browser)
# Инициализируем фрейм создания дочернего элемента (вынесено в атрибуты класса)
self.create_child_frame = CreateElementFrame(browser)
# Проверяем существование стойки # Проверяем существование стойки
if not self._check_rack_existance(browser, RACK_NAME): if not self._check_rack_existance(browser, self.RACK_NAME):
self._create_rack(browser, RACK_NAME) logger.info(f"Rack '{self.RACK_NAME}' does not exist. Creating...")
rack_data = CreateRackData(
name=self.RACK_NAME,
usize="42",
depth="1000"
)
self._create_rack(browser, rack_data)
self.main_page.wait_for_timeout(3000) self.main_page.wait_for_timeout(3000)
else: else:
logger.info(f"Rack '{RACK_NAME}' already exists") logger.info(f"Rack '{self.RACK_NAME}' already exists")
# Переходим к стойке для тестирования # Переходим к стойке для тестирования
self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") self.main_page.click_subpanel_item(self.RACK_NAME, parent="test-zone")
self.main_page.wait_for_timeout(3000) self.main_page.wait_for_timeout(3000)
@pytest.fixture(scope="class", autouse=True) @pytest.fixture(scope="class", autouse=True)
def cleanup_rack(self, browser: Page): def cleanup_rack(self, browser: Page):
"""Фикстура для очистки созданной стойки после ВСЕХ тестов класса. """Фикстура для очистки созданной стойки после ВСЕХ тестов класса.
Выполняется один раз после завершения всех тестов класса TestRackTab. Выполняется один раз после завершения всех тестов класса TestRackEdit.
Удаляет созданную стойку. Удаляет созданную стойку.
Args: Args:
@ -207,6 +110,8 @@ class TestRackTab:
# Тесты выполняются здесь # Тесты выполняются здесь
yield yield
logger.debug(f"Cleaning up rack: {self.RACK_NAME}")
# Переходим на главную страницу и в нужную зону # Переходим на главную страницу и в нужную зону
login_page = LoginPage(browser) login_page = LoginPage(browser)
login_page.do_login() login_page.do_login()
@ -220,37 +125,160 @@ class TestRackTab:
self.main_page.click_main_navigation_panel_item("test-zone") self.main_page.click_main_navigation_panel_item("test-zone")
self.main_page.wait_for_timeout(1000) self.main_page.wait_for_timeout(1000)
# Инициализируем компонент алертов для cleanup
self.alert = AlertComponent(browser)
# Проверяем существование стойки # Проверяем существование стойки
if self._check_rack_existance(browser, RACK_NAME): if self._check_rack_existance(browser, self.RACK_NAME):
# Переходим на страницу стойки # Переходим на страницу стойки
self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") self.main_page.click_subpanel_item(self.RACK_NAME, parent="test-zone")
self.main_page.wait_for_timeout(2000) self.main_page.wait_for_timeout(2000)
# Удаляем стойку # Удаляем стойку
self._delete_rack_from_context_menu(browser, RACK_NAME) self._delete_rack(browser, self.RACK_NAME)
# Дополнительная проверка # Дополнительная проверка
self.main_page.click_subpanel_item("test-zone") self.main_page.click_subpanel_item("test-zone")
self.main_page.wait_for_timeout(1000) self.main_page.wait_for_timeout(1000)
#@pytest.mark.develop def _check_rack_existance(self, browser: Page, rack_name: str) -> bool:
"""Проверяет существование стойки.
Args:
browser: Страница Playwright
rack_name: Имя стойки для проверки
Returns:
bool: True если стойка существует, False в противном случае
"""
logger.debug(f"Checking existence of rack with name '{rack_name}'")
self.main_page.click_subpanel_item("test-zone")
nav_panel_locator = NavigationPanelLocators.TREEVIEW
element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first
if element.is_visible():
logger.debug(f"Rack with name '{rack_name}' found")
return True
logger.debug(f"Rack with name '{rack_name}' not found")
return False
def _create_rack(self, browser: Page, rack_data: CreateRackData) -> None:
"""Создает стойку.
Args:
browser: Страница Playwright
rack_data: Данные стойки для создания
"""
logger.debug(f"Creating rack with name '{rack_data.name}'")
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Нажимаем на плашку "Класс объекта учета"
self.create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
self.create_child_frame.select_object_class("Стойка")
# Создаем форму создания стойки
rack_form = CreateRackForm(browser)
# Заполняем данные стойки
fill_results = rack_form.fill_rack_data(rack_data)
logger.debug(f"Fill results: {fill_results}")
# Нажимаем кнопку создания
self.create_child_frame.click_add_button()
# Ждем появления alert с текстом
expected_alert_text = f"Элемент {rack_data.name} создан"
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
# Если уже закрылся - игнорируем
logger.debug("Alert already closed by the time forcible close was attempted")
logger.info(f"Rack '{rack_data.name}' created successfully")
def _delete_rack(self, browser: Page, rack_name: str) -> None:
"""Удаляет стойку через контекстное меню.
Args:
browser: Страница Playwright
rack_name: Имя стойки для удаления
"""
# Находим элемент стойки в навигационной панели
rack_element = browser.locator(
NavigationPanelLocators.TREEVIEW
).get_by_text(rack_name, exact=True).first
# Прокручиваем до элемента если нужно
rack_element.scroll_into_view_if_needed()
self.main_page.wait_for_timeout(500)
# Проверяем и нажимаем кнопку "Изменить"
rack_page = RackPage(browser)
# Проверяем видимость и тултип кнопки
rack_page.should_be_toolbar_buttons()
# Кликаем на кнопку "Изменить"
rack_page.click_edit_button()
self.main_page.wait_for_timeout(1000)
# Создаем экземпляр EditRackMaker
rack_edit = EditRackMaker(browser, rack_name)
# Используем метод для удаления
rack_edit.click_remove_button()
# Проверяем уведомление об успешном удалении
expected_alert_text = "Успешно удалено"
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
# Если уже закрылся - игнорируем
logger.debug("Alert already closed by the time forcible close was attempted")
logger.info(f"Rack '{rack_name}' deleted successfully")
def test_rack_general_info_tab_fields(self, browser: Page) -> None: def test_rack_general_info_tab_fields(self, browser: Page) -> None:
"""Тест заполнения полей вкладки 'Общая информация' стойки.""" """Тест заполнения полей вкладки 'Общая информация' стойки."""
logger.debug(f"Starting general info tab test for rack: {self.RACK_NAME}")
rack_page = RackPage(browser) rack_page = RackPage(browser)
# Переходим в режим редактирования # Переходим в режим редактирования
rack_page.click_edit_button() rack_page.click_edit_button()
rack_page.wait_for_timeout(1000) rack_page.wait_for_timeout(1000)
# Создаем экземпляр ModalEditRack # Создаем экземпляр EditRackMaker
rack_edit = ModalEditRack(browser, RACK_NAME) rack_edit = EditRackMaker(browser, self.RACK_NAME)
# Создаем тестовые данные для заполнения всех полей # Создаем тестовые данные для заполнения всех полей
rack_edit_data = RackEditData( rack_edit_data = EditRackData(
# Основные поля # Основные поля
name=RACK_NAME, name=self.RACK_NAME,
serial="SN123456789", serial="SN123456789",
inventory="INV987654321", inventory="INV987654321",
comment="Тестовый комментарий для стойки (обновленный)", comment="Тестовый комментарий для стойки (обновленный)",
@ -274,15 +302,19 @@ class TestRackTab:
# Сохраняем изменения # Сохраняем изменения
rack_edit.click_done_button() rack_edit.click_done_button()
rack_edit.wait_for_timeout(2000)
# Проверяем уведомление об успешном обновлении # Проверяем уведомление об успешном обновлении
alert = AlertComponent(browser)
expected_alert_text = "Элемент успешно обновлён" expected_alert_text = "Элемент успешно обновлён"
alert.check_alert_presence(expected_alert_text) self.alert.check_alert_presence(expected_alert_text, timeout=5000)
alert.close_alert_by_text(expected_alert_text)
browser.mouse.click(10, 10) self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
logger.debug("Alert already closed by the time forcible close was attempted")
# Вход в режим редактирования # Вход в режим редактирования
rack_page.click_edit_button() rack_page.click_edit_button()
@ -302,24 +334,27 @@ class TestRackTab:
rack_edit.click_close_button() rack_edit.click_close_button()
#@pytest.mark.develop logger.debug("General info tab test completed successfully")
def test_rack_image_tab(self, browser: Page) -> None: def test_rack_image_tab(self, browser: Page) -> None:
"""Тест вкладки 'Изображение' стойки.""" """Тест вкладки 'Изображение' стойки."""
logger.debug(f"Starting image tab test for rack: {self.RACK_NAME}")
rack_page = RackPage(browser) rack_page = RackPage(browser)
# Переходим в режим редактирования # Переходим в режим редактирования
rack_page.click_edit_button() rack_page.click_edit_button()
rack_page.wait_for_timeout(1000) rack_page.wait_for_timeout(1000)
# Создаем экземпляр ModalEditRack # Создаем экземпляр EditRackMaker
rack_edit = ModalEditRack(browser, RACK_NAME) rack_edit = EditRackMaker(browser, self.RACK_NAME)
# Переключаемся на вкладку "Изображение" # Переключаемся на вкладку "Изображение"
rack_edit.switch_to_tab(ModalEditRack.TAB_IMAGE) rack_edit.switch_to_tab(EditRackMaker.TAB_IMAGE)
# Проверяем вкладку # Проверяем вкладку
assert rack_edit.is_tab_active(ModalEditRack.TAB_IMAGE), "Image tab should be active" assert rack_edit.is_tab_active(EditRackMaker.TAB_IMAGE), "Image tab should be active"
# Загружаем изображение если есть # Загружаем изображение если есть
test_image_path = os.path.join(os.path.dirname(__file__), "test_edit_rack_image.jpg") test_image_path = os.path.join(os.path.dirname(__file__), "test_edit_rack_image.jpg")
@ -339,35 +374,44 @@ class TestRackTab:
# Сохраняем # Сохраняем
rack_edit.click_done_button() rack_edit.click_done_button()
rack_page.wait_for_timeout(2000)
# Проверяем уведомление об успешном обновлении # Проверяем уведомление об успешном обновлении
alert = AlertComponent(browser)
expected_alert_text = "Элемент успешно обновлён" expected_alert_text = "Элемент успешно обновлён"
alert.check_alert_presence(expected_alert_text) self.alert.check_alert_presence(expected_alert_text, timeout=5000)
alert.close_alert_by_text(expected_alert_text)
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
logger.debug("Alert already closed by the time forcible close was attempted")
logger.debug("Image tab test completed successfully")
@pytest.mark.develop
def test_rack_access_rules(self, browser: Page) -> None: def test_rack_access_rules(self, browser: Page) -> None:
"""Тест заполнения полей правил доступа. """Тест заполнения полей правил доступа.
В каждое поле добавляются ВСЕ пользователи из списка custom_users. В каждое поле добавляются ВСЕ пользователи из списка custom_users.
""" """
logger.debug(f"Starting access rules test for rack: {self.RACK_NAME}")
rack_page = RackPage(browser) rack_page = RackPage(browser)
# Переходим в режим редактирования # Переходим в режим редактирования
rack_page.click_edit_button() rack_page.click_edit_button()
rack_page.wait_for_timeout(1000) rack_page.wait_for_timeout(1000)
# Создаем экземпляр ModalEditRack # Создаем экземпляр EditRackMaker
rack_edit = ModalEditRack(browser, RACK_NAME) rack_edit = EditRackMaker(browser, self.RACK_NAME)
# Переключаемся на вкладку "Настройки" # Переключаемся на вкладку "Настройки"
rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS) rack_edit.switch_to_tab(EditRackMaker.TAB_SETTINGS)
# Проверяем, что вкладка активна # Проверяем, что вкладка активна
assert rack_edit.is_tab_active(ModalEditRack.TAB_SETTINGS), \ assert rack_edit.is_tab_active(EditRackMaker.TAB_SETTINGS), \
"Settings tab should be active after switching" "Settings tab should be active after switching"
# Целевые поля для заполнения # Целевые поля для заполнения
@ -420,20 +464,26 @@ class TestRackTab:
# Сохраняем изменения # Сохраняем изменения
rack_edit.click_done_button() rack_edit.click_done_button()
rack_page.wait_for_timeout(2000)
# Проверяем уведомление об успешном обновлении # Проверяем уведомление об успешном обновлении
alert = AlertComponent(browser)
expected_alert_text = "Элемент успешно обновлён" expected_alert_text = "Элемент успешно обновлён"
alert.check_alert_presence(expected_alert_text) self.alert.check_alert_presence(expected_alert_text, timeout=5000)
alert.close_alert_by_text(expected_alert_text)
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
logger.debug("Alert already closed by the time forcible close was attempted")
# Возвращаемся в режим редактирования и проверяем снова # Возвращаемся в режим редактирования и проверяем снова
rack_page.click_edit_button() rack_page.click_edit_button()
rack_page.wait_for_timeout(1000) rack_page.wait_for_timeout(1000)
rack_edit = ModalEditRack(browser, RACK_NAME) rack_edit = EditRackMaker(browser, self.RACK_NAME)
rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS) rack_edit.switch_to_tab(EditRackMaker.TAB_SETTINGS)
verification_results_after_save = rack_edit.verify_access_rules( verification_results_after_save = rack_edit.verify_access_rules(
expected_users=custom_users, expected_users=custom_users,
@ -445,3 +495,5 @@ class TestRackTab:
f"After save - correctly filled {verification_results_after_save['correctly_filled']} out of {expected_total}" f"After save - correctly filled {verification_results_after_save['correctly_filled']} out of {expected_total}"
rack_edit.click_close_button() rack_edit.click_close_button()
logger.debug("Access rules test completed successfully")

View File

@ -6,149 +6,37 @@
import pytest import pytest
from playwright.sync_api import Page from playwright.sync_api import Page
from tools.logger import get_logger
from locators.navigation_panel_locators import NavigationPanelLocators from locators.navigation_panel_locators import NavigationPanelLocators
from components_derived.accounting_objects.rack_maker import RackObjectMaker, RackData from frames.create_element_frame import CreateElementFrame
from components_derived.frames.create_child_element_frame import CreateChildElementFrame from forms.create_rack_form import CreateRackForm, CreateRackData
from components_derived.modal_edit_rack import ModalEditRack from makers.edit_rack_maker import EditRackMaker
from pages.location_page import LocationPage from pages.location_page import LocationPage
from pages.login_page import LoginPage from pages.login_page import LoginPage
from pages.main_page import MainPage from pages.main_page import MainPage
from pages.rack_page import RackPage from pages.rack_page import RackPage
from components.alert_component import AlertComponent from components.alert_component import AlertComponent
from tools.logger import get_logger
# Константы
RACK_NAME = "Test-Rack-Functionality"
# Инициализация логгера для всего модуля
logger = get_logger("RACK_MANAGEMENT_TESTS") logger = get_logger("RACK_MANAGEMENT_TESTS")
logger.setLevel("INFO") logger.setLevel("INFO")
class TestRackTab:
class TestRackManagement:
"""Набор тестов для вкладки 'Стойка' в модуле Объекты. """Набор тестов для вкладки 'Стойка' в модуле Объекты.
Проверяет корректность отображения, функциональность элементов интерфейса Проверяет корректность отображения, функциональность элементов интерфейса
и переключение между вкладками стойки оборудования. и переключение между вкладками стойки оборудования.
Тесты покрывают следующие функциональные области:
1. test_rack_tab_content - Базовая структура и содержимое вкладки стойки
""" """
# Имя тестовой стойки
RACK_NAME = "Test-Rack-Functionality"
# Инициализируем атрибуты # Инициализируем атрибуты
main_page: MainPage = None main_page: MainPage = None
location_page: LocationPage = None location_page: LocationPage = None
alert: AlertComponent = None
def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: create_child_frame: CreateElementFrame = None
"""Проверяет существование стойки.
Args:
browser: Страница Playwright
rack_name: Имя стойки для проверки
Returns:
bool: True если стойка существует, False в противном случае
"""
# Обновляем навигационную панель
self.main_page.wait_for_timeout(500)
self.main_page.click_subpanel_item("test-zone")
nav_panel_locator = NavigationPanelLocators.TREEVIEW
# Проверяем видимость элемента
element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first
if element.is_visible():
return True
return False
def _create_rack(self, browser: Page, rack_name: str) -> None:
"""Создает стойку.
Args:
browser: Страница Playwright
rack_name: Имя стойки для создания
"""
logger.debug(f"Creating rack: {rack_name}")
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Создаем фрейм создания дочернего элемента
create_child_frame = CreateChildElementFrame(browser)
# Нажимаем на плашку "Класс объекта учета"
create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
create_child_frame.select_object_class("Стойка")
# Открывается набор плашек для задания параметров стойки
rack_maker = RackObjectMaker(browser)
# Создаем объект данных стойки
rack_data = RackData(
name=rack_name,
height="42",
depth="1000"
)
# Заполняем данные стойки
rack_maker.fill_rack_data(rack_data)
# Нажимаем кнопку создания
create_child_frame.click_add_button()
# Проверяем уведомление об успешном создании
alert = AlertComponent(browser)
expected_alert_text = f"Элемент {rack_name} создан"
alert.check_alert_presence(expected_alert_text)
alert.close_alert_by_text(expected_alert_text)
logger.info(f"Rack '{rack_name}' created successfully")
def _delete_rack_from_context_menu(self, browser: Page, rack_name: str) -> None:
"""Удаляет стойку через контекстное меню.
Args:
browser: Страница Playwright
rack_name: Имя стойки для удаления
"""
# 1. Находим элемент стойки в навигационной панели
rack_element = browser.locator(
NavigationPanelLocators.TREEVIEW
).get_by_text(rack_name, exact=True).first
# Прокручиваем до элемента если нужно
rack_element.scroll_into_view_if_needed()
self.main_page.wait_for_timeout(500)
# 2. Проверяем и нажимаем кнопку "Изменить"
rack_page = RackPage(browser)
# Проверяем видимость и тултип кнопки
rack_page.should_be_toolbar_buttons()
# Кликаем на кнопку "Изменить"
rack_page.click_edit_button()
self.main_page.wait_for_timeout(1000)
# 3. Создаем экземпляр ModalRackEditRack
rack_edit = ModalEditRack(browser, rack_name)
# Используем метод для удаления
rack_edit.click_remove_button()
self.main_page.wait_for_timeout(1000)
# 4. Проверяем уведомление об успешном удалении
alert = AlertComponent(browser)
expected_alert_text = "Успешно удалено"
alert.check_alert_presence(expected_alert_text)
alert.close_alert_by_text(expected_alert_text)
logger.info(f"Rack '{rack_name}' deleted successfully")
@pytest.fixture(scope="function", autouse=True) @pytest.fixture(scope="function", autouse=True)
def setup(self, browser: Page) -> None: def setup(self, browser: Page) -> None:
@ -156,12 +44,15 @@ class TestRackTab:
Выполняет: Выполняет:
1. Авторизацию в системе 1. Авторизацию в системе
2. Создание стойки если она не существует 2. Переход к локации test-zone
3. Переход к стойке 3. Инициализацию компонентов
4. Создание стойки если она не существует
5. Переход к стойке
Args: Args:
browser (Page): Экземпляр страницы Playwright для взаимодействия с UI browser: Экземпляр страницы Playwright для взаимодействия с UI
""" """
# Авторизация в системе # Авторизация в системе
login_page = LoginPage(browser) login_page = LoginPage(browser)
login_page.do_login() login_page.do_login()
@ -178,15 +69,28 @@ class TestRackTab:
# Создаем экземпляр страницы локации # Создаем экземпляр страницы локации
self.location_page = LocationPage(browser) self.location_page = LocationPage(browser)
# Инициализируем компонент алертов (вынесено в атрибуты класса)
self.alert = AlertComponent(browser)
# Инициализируем фрейм создания дочернего элемента (вынесено в атрибуты класса)
self.create_child_frame = CreateElementFrame(browser)
# Проверяем существование стойки # Проверяем существование стойки
if not self._check_rack_existance(browser, RACK_NAME): if not self._check_rack_existance(browser, self.RACK_NAME):
self._create_rack(browser, RACK_NAME) logger.info(f"Rack '{self.RACK_NAME}' does not exist. Creating...")
rack_data = CreateRackData(
name=self.RACK_NAME,
usize="42",
depth="1000"
)
self._create_rack(browser, rack_data)
self.main_page.wait_for_timeout(3000) self.main_page.wait_for_timeout(3000)
else: else:
logger.info(f"Rack '{RACK_NAME}' already exists") logger.info(f"Rack '{self.RACK_NAME}' already exists")
# Переходим к стойке для тестирования # Переходим к стойке для тестирования
self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") self.main_page.click_subpanel_item(self.RACK_NAME, parent="test-zone")
self.main_page.wait_for_timeout(3000) self.main_page.wait_for_timeout(3000)
@pytest.fixture(scope="class", autouse=True) @pytest.fixture(scope="class", autouse=True)
@ -199,9 +103,12 @@ class TestRackTab:
Args: Args:
browser: Экземпляр страницы Playwright browser: Экземпляр страницы Playwright
""" """
# Тесты выполняются здесь # Тесты выполняются здесь
yield yield
logger.debug(f"Cleaning up rack: {self.RACK_NAME}")
# Переходим на главную страницу и в нужную зону # Переходим на главную страницу и в нужную зону
login_page = LoginPage(browser) login_page = LoginPage(browser)
login_page.do_login() login_page.do_login()
@ -215,21 +122,142 @@ class TestRackTab:
self.main_page.click_main_navigation_panel_item("test-zone") self.main_page.click_main_navigation_panel_item("test-zone")
self.main_page.wait_for_timeout(1000) self.main_page.wait_for_timeout(1000)
# Инициализируем компонент алертов для cleanup
self.alert = AlertComponent(browser)
# Проверяем существование стойки # Проверяем существование стойки
if self._check_rack_existance(browser, RACK_NAME): if self._check_rack_existance(browser, self.RACK_NAME):
# Переходим на страницу стойки # Переходим на страницу стойки
self.main_page.click_subpanel_item(RACK_NAME, parent="test-zone") self.main_page.click_subpanel_item(self.RACK_NAME, parent="test-zone")
self.main_page.wait_for_timeout(2000) self.main_page.wait_for_timeout(2000)
# Удаляем стойку # Удаляем стойку
self._delete_rack_from_context_menu(browser, RACK_NAME) self._delete_rack(browser, self.RACK_NAME)
# Дополнительная проверка # Дополнительная проверка
self.main_page.click_subpanel_item("test-zone") self.main_page.click_subpanel_item("test-zone")
self.main_page.wait_for_timeout(1000) self.main_page.wait_for_timeout(1000)
#@pytest.mark.develop def _check_rack_existance(self, browser: Page, rack_name: str) -> bool:
"""Проверяет существование стойки.
Args:
browser: Страница Playwright
rack_name: Имя стойки для проверки
Returns:
bool: True если стойка существует, False в противном случае
"""
logger.debug(f"Checking existence of rack with name '{rack_name}'")
self.main_page.click_subpanel_item("test-zone")
nav_panel_locator = NavigationPanelLocators.TREEVIEW
element = browser.locator(nav_panel_locator).get_by_text(rack_name, exact=True).first
if element.is_visible():
logger.debug(f"Rack with name '{rack_name}' found")
return True
logger.debug(f"Rack with name '{rack_name}' not found")
return False
def _create_rack(self, browser: Page, rack_data: CreateRackData) -> None:
"""Создает стойку.
Args:
browser: Страница Playwright
rack_data: Данные стойки для создания
"""
logger.debug(f"Creating rack with name '{rack_data.name}'")
# Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button()
# Нажимаем на плашку "Класс объекта учета"
self.create_child_frame.open_object_class_combobox()
# Из выпадающего меню выбираем пункт "Стойка"
self.create_child_frame.select_object_class("Стойка")
# Создаем форму создания стойки
rack_form = CreateRackForm(browser)
# Заполняем данные стойки
fill_results = rack_form.fill_rack_data(rack_data)
logger.debug(f"Fill results: {fill_results}")
# Нажимаем кнопку создания
self.create_child_frame.click_add_button()
# Ждем появления alert с текстом
expected_alert_text = f"Элемент {rack_data.name} создан"
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
# Если уже закрылся - игнорируем
logger.debug("Alert already closed by the time forcible close was attempted")
logger.info(f"Rack '{rack_data.name}' created successfully")
def _delete_rack(self, browser: Page, rack_name: str) -> None:
"""Удаляет стойку через контекстное меню.
Args:
browser: Страница Playwright
rack_name: Имя стойки для удаления
"""
# Находим элемент стойки в навигационной панели
rack_element = browser.locator(
NavigationPanelLocators.TREEVIEW
).get_by_text(rack_name, exact=True).first
# Прокручиваем до элемента если нужно
rack_element.scroll_into_view_if_needed()
self.main_page.wait_for_timeout(500)
# Проверяем и нажимаем кнопку "Изменить"
rack_page = RackPage(browser)
# Проверяем видимость и тултип кнопки
rack_page.should_be_toolbar_buttons()
# Кликаем на кнопку "Изменить"
rack_page.click_edit_button()
self.main_page.wait_for_timeout(1000)
# Создаем экземпляр EditRackMaker
rack_edit = EditRackMaker(browser, rack_name)
# Используем метод для удаления
rack_edit.click_remove_button()
# Проверяем уведомление об успешном удалении
expected_alert_text = "Успешно удалено"
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
self.alert.check_alert_absence(expected_alert_text, timeout=7000)
# Закрываем alert с текстом кнопкой 'Закрыть'
try:
self.alert.close_alert_by_text(expected_alert_text)
logger.debug("Alert forcibly closed")
except AssertionError:
# Если уже закрылся - игнорируем
logger.debug("Alert already closed by the time forcible close was attempted")
logger.info(f"Rack '{rack_name}' deleted successfully")
def test_rack_tab_content(self, browser: Page) -> None: def test_rack_tab_content(self, browser: Page) -> None:
"""Тест содержимого вкладки 'Стойка'. """Тест содержимого вкладки 'Стойка'.
@ -240,30 +268,34 @@ class TestRackTab:
4. Корректность отображения юнитов и устройств на стойке 4. Корректность отображения юнитов и устройств на стойке
Args: Args:
browser (Page): Экземпляр страницы Playwright для взаимодействия с UI browser: Экземпляр страницы Playwright для взаимодействия с UI
""" """
logger.debug(f"Starting test for rack tab content with rack: {self.RACK_NAME}")
expected_toolbar_subtitles = [ expected_toolbar_subtitles = [
"test-zone", "test-zone",
'chevron_right', 'chevron_right',
RACK_NAME self.RACK_NAME
] ]
rt = RackPage(browser) rack_page = RackPage(browser)
rt.should_be_panel_header(expected_toolbar_subtitles) rack_page.should_be_panel_header(expected_toolbar_subtitles)
# Комплексная проверка отображения обеих сторон стойки с детальной информацией # Комплексная проверка отображения обеих сторон стойки с детальной информацией
rt.should_be_rack_sides_displayed() rack_page.should_be_rack_sides_displayed()
# Проверка кнопки "Скрыть стойку" # Проверка кнопки "Скрыть стойку"
rt.should_have_hide_rack_button() rack_page.should_have_hide_rack_button()
# Проверка кнопки "Показать стойку" # Проверка кнопки "Показать стойку"
rt.should_have_show_rack_button() rack_page.should_have_show_rack_button()
# Проверяем переключение между всеми вкладками стойки # Проверяем переключение между всеми вкладками стойки
rt.check_tab_switching() rack_page.check_tab_switching()
# Переход в режим редактирования # Проверям наличие кнопки редактирования
rt.should_be_toolbar_buttons() rack_page.should_be_toolbar_buttons()
rt.wait_for_timeout(1000) rack_page.wait_for_timeout(1000)
logger.debug("Test for rack tab content completed successfully")