Compare commits
1 Commits
main
...
ra5/create
| Author | SHA1 | Date |
|---|---|---|
|
|
da8bde15a1 |
|
|
@ -123,12 +123,13 @@ class AlertComponent(BaseComponent):
|
|||
).filter(has_text=text)).to_be_hidden(timeout=timeout), msg
|
||||
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-окна с заданным текстом.
|
||||
|
||||
Args:
|
||||
text: Текст для проверки. Если пустая строка - проверяет только
|
||||
наличие окна.
|
||||
timeout: Время ожидания появления alert в миллисекундах
|
||||
|
||||
Raises:
|
||||
AssertionError: Если alert-окно не найдено.
|
||||
|
|
@ -136,12 +137,12 @@ class AlertComponent(BaseComponent):
|
|||
|
||||
msg = "Alert window is missing"
|
||||
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")
|
||||
else:
|
||||
expect(self.page.get_by_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")
|
||||
|
||||
def check_text(self, alert_text: str) -> None:
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Binary file not shown.
|
|
@ -11,11 +11,11 @@ from components.toolbar_component import ToolbarComponent
|
|||
from components_derived.selection_bar_component import SelectionBarComponent
|
||||
|
||||
|
||||
logger = get_logger("CREATE_CHILD_ELEMENT_FRAME")
|
||||
|
||||
logger = get_logger("CREATE_ELEMENT_FRAME")
|
||||
logger.setLevel("INFO")
|
||||
|
||||
class CreateChildElementFrame(BaseComponent):
|
||||
|
||||
class CreateElementFrame(BaseComponent):
|
||||
"""Фрейм создания дочернего элемента."""
|
||||
|
||||
def __init__(self, page: Page) -> None:
|
||||
|
|
@ -25,7 +25,6 @@ class CreateChildElementFrame(BaseComponent):
|
|||
Args:
|
||||
page (Page): Экземпляр страницы Playwright
|
||||
"""
|
||||
|
||||
super().__init__(page)
|
||||
|
||||
# Инициализация компонентов
|
||||
|
|
@ -38,7 +37,7 @@ class CreateChildElementFrame(BaseComponent):
|
|||
has_text="Создать дочерний элемент в"
|
||||
).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)
|
||||
|
|
@ -56,25 +55,20 @@ class CreateChildElementFrame(BaseComponent):
|
|||
Args:
|
||||
field_name (str): Название поля для очистки
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
if field_name not in fields_locators:
|
||||
logger.warning(f"Field '{field_name}' not found in form")
|
||||
return
|
||||
|
||||
# Получаем контейнер поля
|
||||
field_container = fields_locators[field_name]
|
||||
|
||||
# Прокручиваем до поля
|
||||
field_container.scroll_into_view_if_needed()
|
||||
self.wait_for_timeout(300)
|
||||
|
||||
# Проверяем видимость
|
||||
if not field_container.is_visible():
|
||||
logger.debug(f"Field '{field_name}' is not visible after scrolling")
|
||||
return
|
||||
|
|
@ -82,11 +76,7 @@ class CreateChildElementFrame(BaseComponent):
|
|||
# Ищем кнопку закрытия (крестик) внутри контейнера поля
|
||||
close_button = field_container.locator("i.mdi-close").first
|
||||
|
||||
# Проверяем наличие и видимость кнопки закрытия
|
||||
if close_button.count() > 0:
|
||||
logger.debug(f"Found close button for field '{field_name}'")
|
||||
|
||||
# Если кнопка закрытия видима - кликаем на нее
|
||||
close_button.click(force=True)
|
||||
self.wait_for_timeout(300)
|
||||
logger.debug(f"Combobox field '{field_name}' cleared using close button")
|
||||
|
|
@ -95,13 +85,11 @@ class CreateChildElementFrame(BaseComponent):
|
|||
|
||||
def click_add_button(self) -> None:
|
||||
"""Кликает на кнопку 'Добавить'."""
|
||||
|
||||
logger.debug("Clicking on 'Add' button...")
|
||||
self.toolbar.click_button("add")
|
||||
|
||||
def click_cancel_button(self) -> None:
|
||||
"""Кликает на кнопку 'Отменить'."""
|
||||
|
||||
logger.debug("Clicking on 'Cancel' button...")
|
||||
self.toolbar.click_button("cancel")
|
||||
|
||||
|
|
@ -112,7 +100,6 @@ class CreateChildElementFrame(BaseComponent):
|
|||
Returns:
|
||||
str: Выбранный класс объекта или пустая строка если ничего не выбрано
|
||||
"""
|
||||
|
||||
return self.selection_bar.get_selection_bar_title()
|
||||
|
||||
def is_field_filled(self, field_name: str, container_locator: Locator = None) -> bool:
|
||||
|
|
@ -126,38 +113,28 @@ class CreateChildElementFrame(BaseComponent):
|
|||
Returns:
|
||||
bool: True если поле заполнено, False в противном случае
|
||||
"""
|
||||
|
||||
logger.debug(f"Checking if field '{field_name}' is filled...")
|
||||
|
||||
# Если контейнер не передан, используем контейнер по умолчанию
|
||||
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)
|
||||
|
||||
if field_name not in fields_locators:
|
||||
logger.debug(f"Field '{field_name}' not found in fields_locators")
|
||||
return False
|
||||
|
||||
# Получаем контейнер поля
|
||||
field_container = fields_locators[field_name]
|
||||
|
||||
if not field_container.is_visible():
|
||||
logger.debug(f"Field '{field_name}' not visible")
|
||||
return False
|
||||
|
||||
# Проверяем наличие выбранного значения через v-chip (чип выбранного значения в combobox)
|
||||
selected_chip = field_container.locator(".v-chip").first
|
||||
|
||||
# Проверяем наличие текста в поле
|
||||
field_text = field_container.text_content() or ""
|
||||
has_text = bool(field_text.strip())
|
||||
|
||||
# Проверяем наличие чипа
|
||||
has_chip = selected_chip.count() > 0 and selected_chip.is_visible()
|
||||
|
||||
# Для текстовых полей проверяем значение input
|
||||
if not has_chip:
|
||||
input_field = field_container.locator("input").first
|
||||
if input_field.count() > 0:
|
||||
|
|
@ -167,13 +144,11 @@ class CreateChildElementFrame(BaseComponent):
|
|||
has_text = has_text or has_input_value
|
||||
|
||||
logger.debug(f"Field '{field_name}' - has chip: {has_chip}, has text: {has_text}")
|
||||
|
||||
return has_chip or has_text
|
||||
|
||||
def open_object_class_combobox(self) -> None:
|
||||
"""Открывает выпадающий список combobox."""
|
||||
|
||||
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER)
|
||||
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
|
||||
fields_locators = self.get_input_fields_locators(container_locator)
|
||||
combobox_container = fields_locators.get("Класс объекта учета")
|
||||
|
||||
|
|
@ -181,12 +156,10 @@ class CreateChildElementFrame(BaseComponent):
|
|||
logger.error("Combobox 'Класс объекта учета' not found")
|
||||
return
|
||||
|
||||
# Проверяем, не открыт ли уже выпадающий список
|
||||
menu_selector = "div.v-menu__content.menuable__content__active"
|
||||
is_menu_open = self.page.locator(menu_selector).count() > 0
|
||||
|
||||
if not is_menu_open:
|
||||
# Используем OPEN_PARAMETERS_LIST_BUTTON из SelectionBarLocators
|
||||
open_button = combobox_container.locator(SelectionBarLocators.OPEN_PARAMETERS_LIST_BUTTON)
|
||||
open_button.click(force=True, timeout=5000)
|
||||
else:
|
||||
|
|
@ -199,18 +172,10 @@ class CreateChildElementFrame(BaseComponent):
|
|||
Args:
|
||||
class_name (str): Название класса объекта для выбора
|
||||
"""
|
||||
|
||||
logger.debug(f"Selecting object class: '{class_name}'...")
|
||||
|
||||
# Открываем combobox
|
||||
self.open_object_class_combobox()
|
||||
|
||||
# Выбирает значение из списка
|
||||
self.selection_bar.select_value(class_name)
|
||||
|
||||
# Даем время на применение выбора
|
||||
self.wait_for_timeout(300)
|
||||
|
||||
logger.debug(f"Object class '{class_name}' successfully selected")
|
||||
|
||||
# Проверки:
|
||||
|
|
@ -226,23 +191,16 @@ class CreateChildElementFrame(BaseComponent):
|
|||
ValueError: Если поле не найдено в форме
|
||||
AssertionError: Если поле не подсвечено ошибкой
|
||||
"""
|
||||
|
||||
logger.debug(f"Checking field '{field_name}' for error highlighting...")
|
||||
|
||||
# Получаем контейнеры всех полей
|
||||
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER)
|
||||
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
|
||||
fields_locators = self.get_input_fields_locators(container_locator)
|
||||
|
||||
# Получаем контейнер конкретного поля
|
||||
field_container = fields_locators.get(field_name)
|
||||
|
||||
if not field_container:
|
||||
raise ValueError(f"Field '{field_name}' not found in form")
|
||||
|
||||
# Ищем элементы с классами ошибки внутри контейнера поля
|
||||
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
|
||||
|
||||
# Проверяем, что есть хотя бы один элемент с классом ошибки
|
||||
has_error = error_elements.count() > 0
|
||||
|
||||
assert has_error, (
|
||||
|
|
@ -263,23 +221,16 @@ class CreateChildElementFrame(BaseComponent):
|
|||
ValueError: Если поле не найдено в форме
|
||||
AssertionError: Если поле подсвечено ошибкой
|
||||
"""
|
||||
|
||||
logger.debug(f"Checking field '{field_name}' for absence of error highlighting...")
|
||||
|
||||
# Получаем контейнеры всех полей
|
||||
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER)
|
||||
container_locator = self.page.locator(RackLocators.CREATE_RACK_FORM_CONTAINER)
|
||||
fields_locators = self.get_input_fields_locators(container_locator)
|
||||
|
||||
# Получаем контейнер конкретного поля
|
||||
field_container = fields_locators.get(field_name)
|
||||
|
||||
if not field_container:
|
||||
raise ValueError(f"Field '{field_name}' not found in form")
|
||||
|
||||
# Ищем элементы с классами ошибки внутри контейнера поля
|
||||
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
|
||||
|
||||
# Проверяем, что нет элементов с классами ошибки
|
||||
has_error = error_elements.count() > 0
|
||||
|
||||
assert not has_error, (
|
||||
|
|
@ -299,7 +250,6 @@ class CreateChildElementFrame(BaseComponent):
|
|||
Raises:
|
||||
AssertionError: Если выбранный класс не соответствует ожидаемому
|
||||
"""
|
||||
|
||||
logger.debug(f"Checking selected object class: '{expected_class}'...")
|
||||
|
||||
self.wait_for_timeout(500)
|
||||
|
|
@ -313,10 +263,7 @@ class CreateChildElementFrame(BaseComponent):
|
|||
f"Expected: '{expected_class}', Got: '{actual_class}'"
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Object class '{expected_class}' successfully selected "
|
||||
f"(actual: '{actual_class}')"
|
||||
)
|
||||
logger.debug(f"Object class '{expected_class}' successfully selected (actual: '{actual_class}')")
|
||||
|
||||
def check_toolbar_title(self, expected_title: str) -> None:
|
||||
"""
|
||||
|
|
@ -328,17 +275,14 @@ class CreateChildElementFrame(BaseComponent):
|
|||
Raises:
|
||||
AssertionError: Если заголовок не соответствует ожидаемому
|
||||
"""
|
||||
|
||||
logger.debug(f"Checking toolbar title: '{expected_title}'...")
|
||||
|
||||
# Используем метод тулбара с фильтрацией по тексту
|
||||
actual_text = self.toolbar.get_toolbar_title_text(
|
||||
filter_text="Создать дочерний элемент в"
|
||||
)
|
||||
|
||||
assert expected_title in actual_text, (
|
||||
f"Title does not match. Expected: '{expected_title}', "
|
||||
f"Got: '{actual_text}'"
|
||||
f"Title does not match. Expected: '{expected_title}', Got: '{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_tooltip("add", "Добавить")
|
||||
self.toolbar.check_button_visibility("cancel")
|
||||
|
|
@ -28,33 +28,54 @@ class RackLocators:
|
|||
ACTIVE_TAB = ("//div[@data-testid='CABINET_SHOW__tabs']"
|
||||
"//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']"
|
||||
|
||||
# Локаторы полей формы
|
||||
INPUT_FORM_RACK_DATA = f"{RACK_EDIT_FORM}"
|
||||
INPUT_FORM_RACK_DATA_FIELD_NAME = "[data-testid='cabinet-bar__main__text-field__name']"
|
||||
INPUT_FORM_RACK_DATA_FIELD_COMMENT = "[data-testid='cabinet-bar__main__text-field__comment']"
|
||||
INPUT_FORM_RACK_DATA_FIELD_SERIAL = "[data-testid='cabinet-bar__main__text-field__serial_number']"
|
||||
INPUT_FORM_RACK_DATA_FIELD_INVENTORY = "[data-testid='cabinet-bar__main__text-field__inventory_number']"
|
||||
INPUT_FORM_RACK_DATA_FIELD_POWER = "[data-testid='cabinet-bar__main__text-field__allocated_power']"
|
||||
# Text
|
||||
EDIT_RACK_FORM_FIELD_NAME = "[data-testid='cabinet-bar__main__text-field__name']"
|
||||
EDIT_RACK_FORM_FIELD_COMMENT = "[data-testid='cabinet-bar__main__text-field__comment']"
|
||||
EDIT_RACK_FORM_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']"
|
||||
EDIT_RACK_FORM_FIELD_POWER = "[data-testid='cabinet-bar__main__text-field__allocated_power']"
|
||||
|
||||
# Локаторы для combobox полей
|
||||
INPUT_FORM_RACK_DATA_FIELD_CABLE_ENTRY = "[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']"
|
||||
INPUT_FORM_RACK_DATA_FIELD_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']"
|
||||
INPUT_FORM_RACK_DATA_FIELD_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']"
|
||||
INPUT_FORM_RACK_DATA_FIELD_PROJECT = "[data-testid='cabinet-bar__select__select-field__project']"
|
||||
# Сombobox
|
||||
EDIT_RACK_FORM_SELECT_CABLE_INPUT = "[data-testid='cabinet-bar__select_enum__select-field__cable_input']"
|
||||
EDIT_RACK_FORM_SELECT_CONDITION_TYPE = "[data-testid='cabinet-bar__select_enum__select-field__condition_type']"
|
||||
EDIT_RACK_FORM_SELECT_DEPTH = "[data-testid='cabinet-bar__select_enum__select-field__depth']"
|
||||
EDIT_RACK_FORM_SELECT_USIZE = "[data-testid='cabinet-bar__select_enum__select-field__usize']"
|
||||
EDIT_RACK_FORM_SELECT_OWNER = "[data-testid='cabinet-bar__select__select-field__owner']"
|
||||
EDIT_RACK_FORM_SELECT_SERVICE_PROVIDER = "[data-testid='cabinet-bar__select__select-field__service_provider']"
|
||||
EDIT_RACK_FORM_SELECT_PROJECT = "[data-testid='cabinet-bar__select__select-field__project']"
|
||||
|
||||
# Чекбоксы
|
||||
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel'] input[type='checkbox']"
|
||||
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_LABEL = "label:has-text('Вентиляционная панель')"
|
||||
INPUT_FORM_RACK_DATA_CHECKBOX_VENTILATION_CONTAINER = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel']"
|
||||
# Checkbox
|
||||
EDIT_RACK_FORM_CHECKBOX_VENTILATION = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel'] input[type='checkbox']"
|
||||
EDIT_RACK_FORM_CHECKBOX_VENTILATION_LABEL = "label:has-text('Вентиляционная панель')"
|
||||
EDIT_RACK_FORM_DATA_CHECKBOX_VENTILATION_CONTAINER = "[data-testid='cabinet-bar__main__checkbox__available_ventilation_panel']"
|
||||
|
||||
# ================ ЛОКАТОРЫ ДЛЯ ВЫПАДАЮЩИХ СПИСКОВ ===================
|
||||
|
||||
# Локаторы для меню combobox
|
||||
MENU_ACTIVE_RACK_FORM = "//div[contains(@class, 'menuable__content__active')]"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,63 +1,32 @@
|
|||
"""Модуль для работы с модальным окном редактирования стойки."""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List, Tuple, 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 elements.text_element import Text
|
||||
from elements.checkbox_element import Checkbox
|
||||
from components.modal_window_component import ModalWindowComponent
|
||||
from components.dropdown_list_component import DropdownList
|
||||
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")
|
||||
|
||||
@dataclass
|
||||
class RackEditData:
|
||||
"""Класс для хранения данных редактирования стойки.
|
||||
|
||||
Содержит все возможные поля, которые могут быть изменены
|
||||
в модальном окне редактирования стойки.
|
||||
"""
|
||||
|
||||
# Основные поля (редактируемые)
|
||||
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 = ""
|
||||
# Используем EditRackFormData
|
||||
EditRackData = EditRackFormData
|
||||
|
||||
|
||||
class ModalEditRack(ModalWindowComponent):
|
||||
class EditRackMaker(ModalWindowComponent):
|
||||
"""Компонент для работы с модальным окном редактирования стойки.
|
||||
|
||||
Предоставляет методы для взаимодействия с элементами окна:
|
||||
- переключение между вкладками
|
||||
- заполнение полей общей информации
|
||||
- заполнение полей общей информации (через EditRackForm)
|
||||
- работа с изображениями
|
||||
- настройка правил доступа
|
||||
- сохранение/отмена изменений
|
||||
|
|
@ -69,47 +38,7 @@ class ModalEditRack(ModalWindowComponent):
|
|||
TAB_IMAGE = "Изображение"
|
||||
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 = {
|
||||
"Правила доступа для чтения": (
|
||||
"read_access_rules", "rules_read_input", "rules_read_list"
|
||||
|
|
@ -148,11 +77,11 @@ class ModalEditRack(ModalWindowComponent):
|
|||
super().__init__(page)
|
||||
self.rack_name = rack_name
|
||||
self.page = page
|
||||
self.available_fields = None
|
||||
self.active_tab = self.TAB_GENERAL
|
||||
self.tabs = {}
|
||||
self.content_items = {}
|
||||
self.delete_confirm = None
|
||||
self.edit_form = None
|
||||
|
||||
# Настройка заголовка и кнопки закрытия
|
||||
self.window_title = rack_name
|
||||
|
|
@ -198,101 +127,11 @@ class ModalEditRack(ModalWindowComponent):
|
|||
def _init_general_tab_content(self) -> None:
|
||||
"""Инициализирует содержимое вкладки 'Общая информация'."""
|
||||
|
||||
# Получаем доступные поля формы с помощью базового метода
|
||||
self.available_fields = self.get_input_fields_locators(
|
||||
self.page.locator(RackLocators.INPUT_FORM_RACK_DATA))
|
||||
|
||||
self._init_text_fields()
|
||||
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")
|
||||
# Инициализируем форму редактирования
|
||||
self.edit_form = EditRackForm(self.page)
|
||||
# Копируем content_items из формы
|
||||
self.content_items.update(self.edit_form.content_items)
|
||||
logger.debug("General tab content initialized via EditRackForm")
|
||||
|
||||
def _init_image_tab_content(self) -> None:
|
||||
"""Инициализирует содержимое вкладки 'Изображение'."""
|
||||
|
|
@ -1020,7 +859,7 @@ class ModalEditRack(ModalWindowComponent):
|
|||
save_button.click()
|
||||
logger.debug("Clicked done button")
|
||||
|
||||
def fill_rack_data(self, rack_data: RackEditData) -> dict:
|
||||
def fill_rack_data(self, rack_data: EditRackData) -> dict:
|
||||
"""Заполняет поля формы редактирования стойки.
|
||||
|
||||
Args:
|
||||
|
|
@ -1033,139 +872,23 @@ class ModalEditRack(ModalWindowComponent):
|
|||
if self.active_tab != self.TAB_GENERAL:
|
||||
self.switch_to_tab(self.TAB_GENERAL)
|
||||
|
||||
results = {
|
||||
# Используем форму для заполнения данных
|
||||
if self.edit_form:
|
||||
results = self.edit_form.fill_rack_data(rack_data)
|
||||
logger.info(f"Filled rack data via EditRackForm: {results}")
|
||||
return results
|
||||
else:
|
||||
logger.error("Edit form not initialized")
|
||||
return {
|
||||
"text_fields_filled": 0,
|
||||
"combobox_fields_filled": 0,
|
||||
"checkboxes_set": 0
|
||||
}
|
||||
|
||||
self._fill_text_fields(rack_data, results)
|
||||
self._fill_combobox_fields(rack_data, results)
|
||||
self._set_checkbox(rack_data, results)
|
||||
|
||||
return results
|
||||
|
||||
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(
|
||||
self,
|
||||
rack_data: RackEditData,
|
||||
rack_data: EditRackData,
|
||||
skip_fields: Optional[List[str]] = None
|
||||
) -> dict:
|
||||
"""Проверяет, что все поля заполнены корректно.
|
||||
|
|
@ -1193,8 +916,13 @@ class ModalEditRack(ModalWindowComponent):
|
|||
if skip_fields is None:
|
||||
skip_fields = []
|
||||
|
||||
# Проверяем текстовые поля
|
||||
self._verify_text_fields(rack_data, skip_fields, results)
|
||||
|
||||
# Проверяем combobox поля
|
||||
self._verify_combobox_fields(rack_data, skip_fields, results)
|
||||
|
||||
# Проверяем чекбокс
|
||||
self._verify_checkbox(rack_data, skip_fields, results)
|
||||
|
||||
if results["total_expected_fields"] > 0:
|
||||
|
|
@ -1208,7 +936,7 @@ class ModalEditRack(ModalWindowComponent):
|
|||
|
||||
def _verify_text_fields(
|
||||
self,
|
||||
rack_data: RackEditData,
|
||||
rack_data: EditRackData,
|
||||
skip_fields: List[str],
|
||||
results: dict
|
||||
) -> None:
|
||||
|
|
@ -1220,7 +948,11 @@ class ModalEditRack(ModalWindowComponent):
|
|||
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, "")
|
||||
if not expected_value or not str(expected_value).strip():
|
||||
continue
|
||||
|
|
@ -1250,7 +982,7 @@ class ModalEditRack(ModalWindowComponent):
|
|||
"""
|
||||
|
||||
try:
|
||||
input_field = self.get_content_item(field_name)
|
||||
input_field = self.edit_form.get_content_item(field_name)
|
||||
if not input_field:
|
||||
results["not_filled"] += 1
|
||||
results["field_errors"].append(f"Field '{field_label}' input not found")
|
||||
|
|
@ -1270,7 +1002,7 @@ class ModalEditRack(ModalWindowComponent):
|
|||
|
||||
def _verify_combobox_fields(
|
||||
self,
|
||||
rack_data: RackEditData,
|
||||
rack_data: EditRackData,
|
||||
skip_fields: List[str],
|
||||
results: dict
|
||||
) -> None:
|
||||
|
|
@ -1282,7 +1014,11 @@ class ModalEditRack(ModalWindowComponent):
|
|||
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, "")
|
||||
if not expected_value or not str(expected_value).strip():
|
||||
continue
|
||||
|
|
@ -1335,8 +1071,12 @@ class ModalEditRack(ModalWindowComponent):
|
|||
Значение поля или пустая строка.
|
||||
"""
|
||||
|
||||
if not self.edit_form:
|
||||
return ""
|
||||
|
||||
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:
|
||||
return actual_value
|
||||
|
|
@ -1357,7 +1097,7 @@ class ModalEditRack(ModalWindowComponent):
|
|||
|
||||
def _verify_checkbox(
|
||||
self,
|
||||
rack_data: RackEditData,
|
||||
rack_data: EditRackData,
|
||||
skip_fields: List[str],
|
||||
results: dict
|
||||
) -> None:
|
||||
|
|
@ -1379,7 +1119,7 @@ class ModalEditRack(ModalWindowComponent):
|
|||
return
|
||||
|
||||
try:
|
||||
checkbox = self.get_content_item("ventilation_checkbox")
|
||||
checkbox = self.edit_form.get_content_item("ventilation_checkbox")
|
||||
if not checkbox:
|
||||
results["not_filled"] += 1
|
||||
results["field_errors"].append("Checkbox 'Ventilation panel' not found")
|
||||
|
|
@ -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}' присутствует в списке")
|
||||
|
|
@ -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}' корректно не подсвечено цветом ошибки")
|
||||
|
|
@ -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} определенных вкладок успешно переключены!")
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -1,157 +1,45 @@
|
|||
"""Модуль тестов вкладки 'Стойка' в модуле Объекты.
|
||||
"""Модуль тестов редактирования стойки в модуле Объекты.
|
||||
|
||||
Содержит тесты для проверки функциональности
|
||||
работы со стойкой оборудования.
|
||||
редактирования стойки оборудования.
|
||||
"""
|
||||
|
||||
import os
|
||||
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 makers.edit_rack_maker import EditRackMaker, EditRackData
|
||||
from pages.location_page import LocationPage
|
||||
from pages.login_page import LoginPage
|
||||
from pages.main_page import MainPage
|
||||
from pages.location_page import LocationPage
|
||||
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 tools.logger import get_logger
|
||||
|
||||
# Константы
|
||||
RACK_NAME = "Test-Rack-Functionality"
|
||||
|
||||
# Инициализация логгера для всего модуля
|
||||
logger = get_logger("RACK_EDIT_TESTS")
|
||||
logger.setLevel("INFO")
|
||||
|
||||
class TestRackTab:
|
||||
"""Набор тестов для вкладки 'Стойка' в модуле Объекты.
|
||||
|
||||
Проверяет корректность отображения, функциональность элементов интерфейса
|
||||
и переключение между вкладками стойки оборудования.
|
||||
class TestRackEdit:
|
||||
"""Набор тестов для редактирования стойки в модуле Объекты.
|
||||
|
||||
Тесты покрывают следующие функциональные области:
|
||||
1. test_rack_general_info_tab_fields - Заполнение полей вкладки 'Общая информация'
|
||||
2. test_rack_image_tab - Работа с вкладкой 'Изображение'
|
||||
3. test_rack_access_rules - Заполнение полей правил доступа
|
||||
Проверяет функциональность редактирования различных вкладок стойки:
|
||||
1. Общая информация
|
||||
2. Изображение
|
||||
3. Правила доступа
|
||||
"""
|
||||
|
||||
# Имя тестовой стойки
|
||||
RACK_NAME = "Test-Rack-Edit"
|
||||
|
||||
# Инициализируем атрибуты
|
||||
main_page: MainPage = None
|
||||
location_page: LocationPage = None
|
||||
|
||||
def _check_rack_existance(self, browser: Page, rack_name: str) -> bool:
|
||||
"""Проверяет существование стойки.
|
||||
|
||||
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")
|
||||
alert: AlertComponent = None
|
||||
create_child_frame: CreateElementFrame = None
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def setup(self, browser: Page) -> None:
|
||||
|
|
@ -159,11 +47,13 @@ class TestRackTab:
|
|||
|
||||
Выполняет:
|
||||
1. Авторизацию в системе
|
||||
2. Создание стойки если она не существует
|
||||
3. Переход к стойке
|
||||
2. Переход к локации test-zone
|
||||
3. Инициализацию компонентов
|
||||
4. Создание стойки если она не существует
|
||||
5. Переход к стойке
|
||||
|
||||
Args:
|
||||
browser (Page): Экземпляр страницы Playwright для взаимодействия с UI
|
||||
browser: Экземпляр страницы Playwright для взаимодействия с UI
|
||||
"""
|
||||
|
||||
# Авторизация в системе
|
||||
|
|
@ -182,22 +72,35 @@ class TestRackTab:
|
|||
# Создаем экземпляр страницы локации
|
||||
self.location_page = LocationPage(browser)
|
||||
|
||||
# Инициализируем компонент алертов (вынесено в атрибуты класса)
|
||||
self.alert = AlertComponent(browser)
|
||||
|
||||
# Инициализируем фрейм создания дочернего элемента (вынесено в атрибуты класса)
|
||||
self.create_child_frame = CreateElementFrame(browser)
|
||||
|
||||
# Проверяем существование стойки
|
||||
if not self._check_rack_existance(browser, RACK_NAME):
|
||||
self._create_rack(browser, RACK_NAME)
|
||||
if not self._check_rack_existance(browser, self.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)
|
||||
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)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def cleanup_rack(self, browser: Page):
|
||||
"""Фикстура для очистки созданной стойки после ВСЕХ тестов класса.
|
||||
|
||||
Выполняется один раз после завершения всех тестов класса TestRackTab.
|
||||
Выполняется один раз после завершения всех тестов класса TestRackEdit.
|
||||
Удаляет созданную стойку.
|
||||
|
||||
Args:
|
||||
|
|
@ -207,6 +110,8 @@ class TestRackTab:
|
|||
# Тесты выполняются здесь
|
||||
yield
|
||||
|
||||
logger.debug(f"Cleaning up rack: {self.RACK_NAME}")
|
||||
|
||||
# Переходим на главную страницу и в нужную зону
|
||||
login_page = LoginPage(browser)
|
||||
login_page.do_login()
|
||||
|
|
@ -220,37 +125,160 @@ class TestRackTab:
|
|||
self.main_page.click_main_navigation_panel_item("test-zone")
|
||||
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._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.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:
|
||||
"""Тест заполнения полей вкладки 'Общая информация' стойки."""
|
||||
|
||||
logger.debug(f"Starting general info tab test for rack: {self.RACK_NAME}")
|
||||
|
||||
rack_page = RackPage(browser)
|
||||
|
||||
# Переходим в режим редактирования
|
||||
rack_page.click_edit_button()
|
||||
rack_page.wait_for_timeout(1000)
|
||||
|
||||
# Создаем экземпляр ModalEditRack
|
||||
rack_edit = ModalEditRack(browser, RACK_NAME)
|
||||
# Создаем экземпляр EditRackMaker
|
||||
rack_edit = EditRackMaker(browser, self.RACK_NAME)
|
||||
|
||||
# Создаем тестовые данные для заполнения всех полей
|
||||
rack_edit_data = RackEditData(
|
||||
rack_edit_data = EditRackData(
|
||||
# Основные поля
|
||||
name=RACK_NAME,
|
||||
name=self.RACK_NAME,
|
||||
serial="SN123456789",
|
||||
inventory="INV987654321",
|
||||
comment="Тестовый комментарий для стойки (обновленный)",
|
||||
|
|
@ -274,15 +302,19 @@ class TestRackTab:
|
|||
|
||||
# Сохраняем изменения
|
||||
rack_edit.click_done_button()
|
||||
rack_edit.wait_for_timeout(2000)
|
||||
|
||||
# Проверяем уведомление об успешном обновлении
|
||||
alert = AlertComponent(browser)
|
||||
expected_alert_text = "Элемент успешно обновлён"
|
||||
alert.check_alert_presence(expected_alert_text)
|
||||
alert.close_alert_by_text(expected_alert_text)
|
||||
self.alert.check_alert_presence(expected_alert_text, timeout=5000)
|
||||
|
||||
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()
|
||||
|
|
@ -302,24 +334,27 @@ class TestRackTab:
|
|||
|
||||
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:
|
||||
"""Тест вкладки 'Изображение' стойки."""
|
||||
|
||||
logger.debug(f"Starting image tab test for rack: {self.RACK_NAME}")
|
||||
|
||||
rack_page = RackPage(browser)
|
||||
|
||||
# Переходим в режим редактирования
|
||||
rack_page.click_edit_button()
|
||||
rack_page.wait_for_timeout(1000)
|
||||
|
||||
# Создаем экземпляр ModalEditRack
|
||||
rack_edit = ModalEditRack(browser, RACK_NAME)
|
||||
# Создаем экземпляр EditRackMaker
|
||||
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")
|
||||
|
|
@ -339,35 +374,44 @@ class TestRackTab:
|
|||
|
||||
# Сохраняем
|
||||
rack_edit.click_done_button()
|
||||
rack_page.wait_for_timeout(2000)
|
||||
|
||||
# Проверяем уведомление об успешном обновлении
|
||||
alert = AlertComponent(browser)
|
||||
expected_alert_text = "Элемент успешно обновлён"
|
||||
alert.check_alert_presence(expected_alert_text)
|
||||
alert.close_alert_by_text(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.debug("Image tab test completed successfully")
|
||||
|
||||
@pytest.mark.develop
|
||||
def test_rack_access_rules(self, browser: Page) -> None:
|
||||
"""Тест заполнения полей правил доступа.
|
||||
|
||||
В каждое поле добавляются ВСЕ пользователи из списка custom_users.
|
||||
"""
|
||||
|
||||
logger.debug(f"Starting access rules test for rack: {self.RACK_NAME}")
|
||||
|
||||
rack_page = RackPage(browser)
|
||||
|
||||
# Переходим в режим редактирования
|
||||
rack_page.click_edit_button()
|
||||
rack_page.wait_for_timeout(1000)
|
||||
|
||||
# Создаем экземпляр ModalEditRack
|
||||
rack_edit = ModalEditRack(browser, RACK_NAME)
|
||||
# Создаем экземпляр EditRackMaker
|
||||
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"
|
||||
|
||||
# Целевые поля для заполнения
|
||||
|
|
@ -420,20 +464,26 @@ class TestRackTab:
|
|||
|
||||
# Сохраняем изменения
|
||||
rack_edit.click_done_button()
|
||||
rack_page.wait_for_timeout(2000)
|
||||
|
||||
# Проверяем уведомление об успешном обновлении
|
||||
alert = AlertComponent(browser)
|
||||
expected_alert_text = "Элемент успешно обновлён"
|
||||
alert.check_alert_presence(expected_alert_text)
|
||||
alert.close_alert_by_text(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")
|
||||
|
||||
# Возвращаемся в режим редактирования и проверяем снова
|
||||
rack_page.click_edit_button()
|
||||
rack_page.wait_for_timeout(1000)
|
||||
|
||||
rack_edit = ModalEditRack(browser, RACK_NAME)
|
||||
rack_edit.switch_to_tab(ModalEditRack.TAB_SETTINGS)
|
||||
rack_edit = EditRackMaker(browser, self.RACK_NAME)
|
||||
rack_edit.switch_to_tab(EditRackMaker.TAB_SETTINGS)
|
||||
|
||||
verification_results_after_save = rack_edit.verify_access_rules(
|
||||
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}"
|
||||
|
||||
rack_edit.click_close_button()
|
||||
|
||||
logger.debug("Access rules test completed successfully")
|
||||
|
|
|
|||
|
|
@ -6,149 +6,37 @@
|
|||
|
||||
import pytest
|
||||
from playwright.sync_api import Page
|
||||
from tools.logger import get_logger
|
||||
from locators.navigation_panel_locators import NavigationPanelLocators
|
||||
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
|
||||
from frames.create_element_frame import CreateElementFrame
|
||||
from forms.create_rack_form import CreateRackForm, CreateRackData
|
||||
from makers.edit_rack_maker import EditRackMaker
|
||||
from pages.location_page import LocationPage
|
||||
from pages.login_page import LoginPage
|
||||
from pages.main_page import MainPage
|
||||
from pages.rack_page import RackPage
|
||||
from components.alert_component import AlertComponent
|
||||
from tools.logger import get_logger
|
||||
|
||||
# Константы
|
||||
RACK_NAME = "Test-Rack-Functionality"
|
||||
|
||||
# Инициализация логгера для всего модуля
|
||||
logger = get_logger("RACK_MANAGEMENT_TESTS")
|
||||
logger.setLevel("INFO")
|
||||
|
||||
class TestRackTab:
|
||||
|
||||
class TestRackManagement:
|
||||
"""Набор тестов для вкладки 'Стойка' в модуле Объекты.
|
||||
|
||||
Проверяет корректность отображения, функциональность элементов интерфейса
|
||||
и переключение между вкладками стойки оборудования.
|
||||
|
||||
Тесты покрывают следующие функциональные области:
|
||||
1. test_rack_tab_content - Базовая структура и содержимое вкладки стойки
|
||||
"""
|
||||
|
||||
# Имя тестовой стойки
|
||||
RACK_NAME = "Test-Rack-Functionality"
|
||||
|
||||
# Инициализируем атрибуты
|
||||
main_page: MainPage = None
|
||||
location_page: LocationPage = None
|
||||
|
||||
def _check_rack_existance(self, browser: Page, rack_name: str) -> bool:
|
||||
"""Проверяет существование стойки.
|
||||
|
||||
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")
|
||||
alert: AlertComponent = None
|
||||
create_child_frame: CreateElementFrame = None
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def setup(self, browser: Page) -> None:
|
||||
|
|
@ -156,12 +44,15 @@ class TestRackTab:
|
|||
|
||||
Выполняет:
|
||||
1. Авторизацию в системе
|
||||
2. Создание стойки если она не существует
|
||||
3. Переход к стойке
|
||||
2. Переход к локации test-zone
|
||||
3. Инициализацию компонентов
|
||||
4. Создание стойки если она не существует
|
||||
5. Переход к стойке
|
||||
|
||||
Args:
|
||||
browser (Page): Экземпляр страницы Playwright для взаимодействия с UI
|
||||
browser: Экземпляр страницы Playwright для взаимодействия с UI
|
||||
"""
|
||||
|
||||
# Авторизация в системе
|
||||
login_page = LoginPage(browser)
|
||||
login_page.do_login()
|
||||
|
|
@ -178,15 +69,28 @@ class TestRackTab:
|
|||
# Создаем экземпляр страницы локации
|
||||
self.location_page = LocationPage(browser)
|
||||
|
||||
# Инициализируем компонент алертов (вынесено в атрибуты класса)
|
||||
self.alert = AlertComponent(browser)
|
||||
|
||||
# Инициализируем фрейм создания дочернего элемента (вынесено в атрибуты класса)
|
||||
self.create_child_frame = CreateElementFrame(browser)
|
||||
|
||||
# Проверяем существование стойки
|
||||
if not self._check_rack_existance(browser, RACK_NAME):
|
||||
self._create_rack(browser, RACK_NAME)
|
||||
if not self._check_rack_existance(browser, self.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)
|
||||
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)
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
|
|
@ -199,9 +103,12 @@ class TestRackTab:
|
|||
Args:
|
||||
browser: Экземпляр страницы Playwright
|
||||
"""
|
||||
|
||||
# Тесты выполняются здесь
|
||||
yield
|
||||
|
||||
logger.debug(f"Cleaning up rack: {self.RACK_NAME}")
|
||||
|
||||
# Переходим на главную страницу и в нужную зону
|
||||
login_page = LoginPage(browser)
|
||||
login_page.do_login()
|
||||
|
|
@ -215,21 +122,142 @@ class TestRackTab:
|
|||
self.main_page.click_main_navigation_panel_item("test-zone")
|
||||
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._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.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:
|
||||
"""Тест содержимого вкладки 'Стойка'.
|
||||
|
||||
|
|
@ -240,30 +268,34 @@ class TestRackTab:
|
|||
4. Корректность отображения юнитов и устройств на стойке
|
||||
|
||||
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 = [
|
||||
"test-zone",
|
||||
'chevron_right',
|
||||
RACK_NAME
|
||||
self.RACK_NAME
|
||||
]
|
||||
|
||||
rt = RackPage(browser)
|
||||
rt.should_be_panel_header(expected_toolbar_subtitles)
|
||||
rack_page = RackPage(browser)
|
||||
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()
|
||||
rt.wait_for_timeout(1000)
|
||||
# Проверям наличие кнопки редактирования
|
||||
rack_page.should_be_toolbar_buttons()
|
||||
rack_page.wait_for_timeout(1000)
|
||||
|
||||
logger.debug("Test for rack tab content completed successfully")
|
||||
|
|
|
|||
Loading…
Reference in New Issue