Обновление компонентов создания элемента Стойка

ra2/create_element_rack
Radislav 2026-01-12 00:21:09 +03:00
parent 410d279e23
commit 8127781d89
6 changed files with 373 additions and 267 deletions

View File

@ -8,6 +8,7 @@ from tools.logger import get_logger
logger = get_logger("BASE_COMPONENT") logger = get_logger("BASE_COMPONENT")
logger.setLevel("INFO")
class BaseComponent: class BaseComponent:
"""Базовый компонент для работы с элементами страницы. """Базовый компонент для работы с элементами страницы.
@ -28,6 +29,42 @@ class BaseComponent:
self.page = page self.page = page
# Действия: # Действия:
def get_input_fields_locators_(self, container_locator: Locator, search_class: str = "layout") -> dict:
"""Получение объекта словаря имя поля ввода : Locator.
Args:
container_locator: объект Locator.
search_class: css класс для поиска (по умолчанию layout)
Returns:
dict: словарь имя поля ввода : Locator xs8 контейнера
"""
fields_locators = {}
if search_class == "layout":
# Поиск по структуре layout -> xs4 (label) + xs8 (input контейнер)
layout_containers = container_locator.locator("div.layout")
for i in range(layout_containers.count()):
layout_container = layout_containers.nth(i)
xs4_div = layout_container.locator("div.flex.xs4").first
xs8_div = layout_container.locator("div.flex.xs8").first
if xs4_div.count() > 0 and xs8_div.count() > 0:
# Ищем input в label_container
label_input = xs4_div.locator("div.v-text-field__slot > input").first
if label_input.count() > 0:
label_text = label_input.input_value().strip()
if label_text:
# Возвращаем xs8 контейнер
fields_locators[label_text] = xs8_div
return fields_locators
def get_input_fields_locators(self, container_locator: Locator, css_class: str) -> dict: def get_input_fields_locators(self, container_locator: Locator, css_class: str) -> dict:
"""Получение объекта словаря имя поля ввода : Locator. """Получение объекта словаря имя поля ввода : Locator.

View File

@ -60,31 +60,79 @@ class RackObjectMaker(BaseComponent):
def _fill_text_fields(self, rack_data: RackData) -> None: def _fill_text_fields(self, rack_data: RackData) -> None:
"""Заполняет текстовые поля.""" """Заполняет текстовые поля."""
def clear_and_fill(locator, value: str, field_name: str): logger.debug("Filling text fields...")
# Получаем контейнер формы (второй элемент)
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")
# Используем метод из базового класса для получения всех полей
fields_locators = self.get_input_fields_locators_(container_locator, "layout")
logger.debug(f"Available text fields: {list(fields_locators.keys())}")
def clear_and_fill(field_name: str, value: str):
"""Очищает поле и заполняет его значением.""" """Очищает поле и заполняет его значением."""
field = self.page.locator(locator).first if not value:
logger.debug(f"Skipping empty value for field '{field_name}'")
return
# Получаем xs8 контейнер поля
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(500)
# Проверяем, не 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
# Очищаем поле # Очищаем поле
field.click() input_field.click()
field.press("Control+A") input_field.press("Control+A")
field.press("Backspace") input_field.press("Backspace")
# Заполняем значение # Заполняем значение
field.fill(value) input_field.fill(value)
logger.debug(f"Filled '{field_name}': {value}") logger.debug(f"Filled '{field_name}': {value}")
# Обязательные поля. # Обязательные поля
if rack_data.name: if rack_data.name:
clear_and_fill(RackLocators.RACK_NAME_FIELD, rack_data.name, "Name") clear_and_fill("Имя", rack_data.name)
# Опциональные поля. # Опциональные поля
if rack_data.serial: if rack_data.serial:
clear_and_fill(RackLocators.RACK_SERIAL_FIELD, rack_data.serial, "Serial number") clear_and_fill("Серийный номер", rack_data.serial)
if rack_data.inventory: if rack_data.inventory:
clear_and_fill(RackLocators.RACK_INVENTORY_FIELD, rack_data.inventory, "Inventory number") clear_and_fill("Инвентарный номер", rack_data.inventory)
if rack_data.comment: if rack_data.comment:
clear_and_fill(RackLocators.RACK_COMMENT_FIELD, rack_data.comment, "Comment") clear_and_fill("Комментарий", rack_data.comment)
logger.debug("Text fields filled successfully")
def _fill_combobox_fields(self, rack_data: RackData) -> None: def _fill_combobox_fields(self, rack_data: RackData) -> None:
"""Заполняет combobox поля.""" """Заполняет combobox поля."""
@ -128,11 +176,20 @@ class RackObjectMaker(BaseComponent):
value: Значение для установки value: Значение для установки
""" """
logger.debug(f"Filling field '{field_name}' with value '{value}'...") # Получаем контейнер формы (второй элемент)
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1)
# Используем универсальный локатор для combobox по имени поля # Используем метод из базового класса BaseComponent
combobox_locator = RackLocators.COMBOBOX_BY_FIELD_NAME.format(field_name) fields_locators = self.get_input_fields_locators_(container_locator, "layout")
field_container = self.page.locator(combobox_locator).first
# Получаем контейнер поля по его названию
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() field_container.scroll_into_view_if_needed()
@ -141,8 +198,11 @@ class RackObjectMaker(BaseComponent):
# Проверяем видимость поля # Проверяем видимость поля
self.check_visibility(field_container, f"Field '{field_name}' not found") self.check_visibility(field_container, f"Field '{field_name}' not found")
# Кликаем и вводим значение # Находим кнопку открытия выпадающего списка внутри контейнера поля
field_container.click(force=True) open_button = field_container.locator(".v-input__append-inner").first
# Кликаем для открытия выпадающего списка
open_button.click(force=True)
self.wait_for_timeout(1000) self.wait_for_timeout(1000)
# Вводим значение из выпадающего списка # Вводим значение из выпадающего списка
@ -198,28 +258,6 @@ class RackObjectMaker(BaseComponent):
self.wait_for_timeout(300) self.wait_for_timeout(300)
def _get_field_locator(self, field_name: str) -> str:
"""
Возвращает локатор поля по его названию.
Args:
field_name: Название поля
Returns:
str: Локатор поля
"""
field_map = {
"Имя": RackLocators.RACK_NAME_FIELD,
"Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD,
"Глубина (мм)": RackLocators.RACK_DEPTH_FIELD
}
if field_name not in field_map:
raise ValueError(f"Field '{field_name}' is not supported")
return field_map[field_name]
# Проверки: # Проверки:
def check_rack_fields_presence(self) -> None: def check_rack_fields_presence(self) -> None:
@ -232,37 +270,53 @@ class RackObjectMaker(BaseComponent):
logger.debug("Checking rack fields presence...") logger.debug("Checking rack fields presence...")
# Основные обязательные поля # Получаем контейнер формы (второй элемент)
required_fields = [ container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1)
(RackLocators.RACK_NAME_FIELD, "Name"),
(RackLocators.RACK_HEIGHT_FIELD, "Height in units"), # Проверяем наличие контейнера
(RackLocators.RACK_DEPTH_FIELD, "Depth (mm)") assert container_locator.count() > 0, "Form container not found"
# Используем метод из базового класса BaseComponent для получения всех полей
fields_locators = self.get_input_fields_locators_(container_locator, "layout")
logger.debug(f"Found fields in form: {list(fields_locators.keys())}")
# Список ожидаемых полей для стойки
expected_fields = [
"Имя",
"Высота в юнитах",
"Глубина (мм)",
"Серийный номер",
"Инвентарный номер",
"Комментарий",
"Ввод кабеля",
"Состояние",
"Владелец",
"Обслуживающая организация",
"Проект/Титул"
] ]
# Дополнительные поля # Проверяем наличие обязательных полей с помощью assert
optional_fields = [ required_fields = ["Имя", "Высота в юнитах", "Глубина (мм)"]
(RackLocators.RACK_SERIAL_FIELD, "Serial number"),
(RackLocators.RACK_INVENTORY_FIELD, "Inventory number"),
(RackLocators.RACK_COMMENT_FIELD, "Comment"),
(RackLocators.RACK_CABLE_ENTRY_FIELD, "Cable entry"),
(RackLocators.RACK_STATE_FIELD, "State"),
(RackLocators.RACK_OWNER_FIELD, "Owner"),
(RackLocators.RACK_SERVICE_ORG_FIELD, "Service organization"),
(RackLocators.RACK_PROJECT_FIELD, "Project/Title")
]
# Проверяем обязательные поля for field_name in required_fields:
for field_locator, field_name in required_fields: # Проверяем наличие поля в словаре
field = self.page.locator(field_locator).first assert field_name in fields_locators, f"Required field '{field_name}' not found"
self.check_visibility(field, f"Required field '{field_name}' not found")
logger.debug(f"Required field '{field_name}' found")
# Проверяем дополнительные поля field_container = fields_locators[field_name]
for field_locator, field_name in optional_fields: # check_visibility внутри использует expect, который тоже вызывает AssertionError
field = self.page.locator(field_locator).first self.check_visibility(field_container, f"Required field '{field_name}' not visible")
if field.count() > 0 and field.is_visible(): logger.debug(f"Required field '{field_name}' found and visible")
logger.debug(f"Optional field '{field_name}' found")
# Проверяем наличие дополнительных полей (только логгирование)
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: else:
logger.debug(f"Optional field '{field_name}' not found or not visible") 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") logger.debug("All main rack fields are present")

View File

@ -1,9 +1,10 @@
"""Модуль фрейма создания дочернего элемента.""" """Модуль фрейма создания дочернего элемента."""
import re import re
from playwright.sync_api import expect, Page from playwright.sync_api import expect, Page, Locator
from tools.logger import get_logger from tools.logger import get_logger
from locators.rack_locators import RackLocators from locators.rack_locators import RackLocators
from locators.selection_bar_locators import SelectionBarLocators
from components.alert_component import AlertComponent from components.alert_component import AlertComponent
from components.base_component import BaseComponent from components.base_component import BaseComponent
from components.toolbar_component import ToolbarComponent from components.toolbar_component import ToolbarComponent
@ -58,11 +59,41 @@ class CreateChildElementFrame(BaseComponent):
logger.debug(f"Clearing combobox field '{field_name}'...") logger.debug(f"Clearing combobox field '{field_name}'...")
# Получаем локатор поля по его названию # Получаем контейнер формы
field_locator = self.get_field_locator(field_name) container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1)
# Используем метод из SelectionBarComponent # Используем метод get_input_fields_locators для получения всех полей
self.selection_bar.clear_combobox_field(field_name, field_locator) fields_locators = self.get_input_fields_locators_(container_locator, "layout")
if field_name not in fields_locators:
logger.warning(f"Field '{field_name}' not found in form")
return
# Получаем xs8 контейнер поля
field_container = fields_locators[field_name]
# Прокручиваем до поля
field_container.scroll_into_view_if_needed()
self.wait_for_timeout(500)
# Проверяем видимость
if not field_container.is_visible():
logger.debug(f"Field '{field_name}' is not visible after scrolling")
return
# Ищем кнопку закрытия (крестик) внутри контейнера поля
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(500)
logger.debug(f"Combobox field '{field_name}' cleared using close button")
else:
logger.debug(f"Close button (i.mdi-close) not found for field '{field_name}'")
def click_add_button(self) -> None: def click_add_button(self) -> None:
"""Кликает на кнопку 'Добавить'.""" """Кликает на кнопку 'Добавить'."""
@ -76,34 +107,6 @@ class CreateChildElementFrame(BaseComponent):
logger.debug("Clicking on 'Cancel' button...") logger.debug("Clicking on 'Cancel' button...")
self.toolbar.click_button("cancel") self.toolbar.click_button("cancel")
def get_field_locator(self, field_name: str) -> str:
"""
Возвращает локатор поля по его названию.
Публичный метод для использования в тестах.
Args:
field_name: Название поля
Returns:
str: Локатор поля
"""
return self._get_field_locator(field_name)
def get_object_class_options(self) -> list[str]:
"""
Получает список доступных опций из combobox.
Returns:
list[str]: Список доступных классов объектов
"""
logger.debug("Getting combobox 'Accounting object class' options...")
available_options = self.selection_bar.get_available_options()
logger.debug(f"Available object class options: {available_options}")
return available_options
def get_selected_object_class(self) -> str: def get_selected_object_class(self) -> str:
""" """
Получает выбранный класс объекта учета. Получает выбранный класс объекта учета.
@ -115,27 +118,28 @@ class CreateChildElementFrame(BaseComponent):
return self.selection_bar.get_selection_bar_title() return self.selection_bar.get_selection_bar_title()
def open_object_class_combobox(self) -> None: def open_object_class_combobox(self) -> None:
"""Открывает выпадающий список combobox 'Класс объекта учета'.""" """Открывает выпадающий список combobox."""
logger.debug("Opening combobox 'Accounting object class'...") container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER)
# Ждем стабильности combobox # Используем метод из базового класса BaseComponent
expect(self.selection_bar.selection_bar_locator).to_be_visible() fields_locators = self.get_input_fields_locators_(container_locator, "layout")
combobox_container = fields_locators.get("Класс объекта учета")
# Проверяем, не открыт ли уже выпадающий список if not combobox_container:
is_menu_active = self.selection_bar.selection_bar_locator.get_attribute( logger.error("Combobox 'Класс объекта учета' not found")
"class"
)
if is_menu_active and "v-select--is-menu-active" in is_menu_active:
logger.debug("Dropdown list is already open")
return return
# Используем force click для обхода перекрывающих элементов # Проверяем, не открыт ли уже выпадающий список
logger.debug("Using force click for combobox") menu_selector = "div.v-menu__content.menuable__content__active"
self.selection_bar.selection_bar_locator.click(force=True) is_menu_open = self.page.locator(menu_selector).count() > 0
# Ждем появления выпадающего списка if not is_menu_open:
self.wait_for_timeout(1500) # Используем OPEN_PARAMETERS_LIST_BUTTON из SelectionBarLocators
open_button = combobox_container.locator(SelectionBarLocators.OPEN_PARAMETERS_LIST_BUTTON)
open_button.click(force=True, timeout=5000)
else:
logger.debug("Combobox menu is already open")
def select_object_class(self, class_name: str) -> None: def select_object_class(self, class_name: str) -> None:
"""Выбирает класс объекта из выпадающего списка.""" """Выбирает класс объекта из выпадающего списка."""
@ -151,13 +155,6 @@ class CreateChildElementFrame(BaseComponent):
# Даем время на применение выбора # Даем время на применение выбора
self.wait_for_timeout(3000) self.wait_for_timeout(3000)
# Логируем текущее состояние без строгой проверки
selected_value = self.get_selected_object_class()
logger.debug(f"Current combobox value: '{selected_value}'")
# Временно пропускаем строгую проверку
logger.debug(f"Assuming class '{class_name}' is selected")
logger.debug(f"Object class '{class_name}' successfully selected") logger.debug(f"Object class '{class_name}' successfully selected")
# Проверки: # Проверки:
@ -170,8 +167,30 @@ class CreateChildElementFrame(BaseComponent):
field_name: Название поля для проверки field_name: Название поля для проверки
""" """
field_locator = self.get_field_locator(field_name) logger.debug(f"Checking field '{field_name}' for error highlighting...")
self.selection_bar.check_field_error_highlighted(field_name, field_locator)
# Получаем контейнеры всех полей
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER)
fields_locators = self.get_input_fields_locators_(container_locator, "layout")
# Получаем контейнер конкретного поля
field_container = fields_locators.get(field_name)
if not field_container:
raise ValueError(f"Field '{field_name}' not found in form")
# Ищем элементы с классами ошибки внутри контейнера поля
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
# Проверяем, что есть хотя бы один элемент с классом ошибки
has_error = error_elements.count() > 0
assert has_error, (
f"Field '{field_name}' has no elements with error classes. "
f"Expected to find elements matching: {SelectionBarLocators.ERROR_CSS_SELECTORS}"
)
logger.debug(f"Field '{field_name}' is correctly highlighted with error color")
def check_field_error_not_highlighted(self, field_name: str) -> None: def check_field_error_not_highlighted(self, field_name: str) -> None:
""" """
@ -181,8 +200,30 @@ class CreateChildElementFrame(BaseComponent):
field_name: Название поля для проверки field_name: Название поля для проверки
""" """
field_locator = self.get_field_locator(field_name) logger.debug(f"Checking field '{field_name}' for absence of error highlighting...")
self.selection_bar.check_field_error_not_highlighted(field_name, field_locator)
# Получаем контейнеры всех полей
container_locator = self.page.locator(RackLocators.FORM_INPUT_CONTAINER)
fields_locators = self.get_input_fields_locators_(container_locator, "layout")
# Получаем контейнер конкретного поля
field_container = fields_locators.get(field_name)
if not field_container:
raise ValueError(f"Field '{field_name}' not found in form")
# Ищем элементы с классами ошибки внутри контейнера поля
error_elements = field_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS)
# Проверяем, что нет элементов с классами ошибки
has_error = error_elements.count() > 0
assert not has_error, (
f"Field '{field_name}' has {error_elements.count()} elements with error classes. "
f"Expected no elements matching: {SelectionBarLocators.ERROR_CSS_SELECTORS}"
)
logger.debug(f"Field '{field_name}' correctly has no error highlighting")
def check_object_class_selected(self, expected_class: str) -> None: def check_object_class_selected(self, expected_class: str) -> None:
""" """
@ -245,33 +286,3 @@ class CreateChildElementFrame(BaseComponent):
self.toolbar.check_button_tooltip("cancel", "Отменить") self.toolbar.check_button_tooltip("cancel", "Отменить")
self.toolbar.click_button("cancel") self.toolbar.click_button("cancel")
self.wait_for_timeout(2000) self.wait_for_timeout(2000)
def _get_field_locator(self, field_name: str) -> str:
"""
Возвращает локатор поля по его названию.
Args:
field_name: Название поля
Returns:
str: Локатор поля
"""
field_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,
"Ввод кабеля": RackLocators.RACK_CABLE_ENTRY_FIELD,
"Состояние": RackLocators.RACK_STATE_FIELD,
"Владелец": RackLocators.RACK_OWNER_FIELD,
"Обслуживающая организация": RackLocators.RACK_SERVICE_ORG_FIELD,
"Проект/Титул": RackLocators.RACK_PROJECT_FIELD
}
if field_name not in field_map:
raise ValueError(f"Locator for field '{field_name}' not found")
return field_map[field_name]

View File

@ -16,61 +16,20 @@ class RackLocators:
- Контейнеры и структурные элементы - Контейнеры и структурные элементы
""" """
# Основной контейнер вкладок стойки (верхние вкладки) # Основной контейнер вкладок стойки (верхние вкладки)
TABS_CONTAINER = "//div[@data-testid='CABINET_SHOW__tabs' and contains(@class, 'v-tabs')]"
# Все элементы верхних вкладок стойки
ALL_TABS = "//div[@data-testid='CABINET_SHOW__tabs']//a[contains(@class, 'v-tabs__item')]" ALL_TABS = "//div[@data-testid='CABINET_SHOW__tabs']//a[contains(@class, 'v-tabs__item')]"
# Кнопка редактирования свойств стойки
EDIT_BUTTON ="//button[@data-testid='CABINET_SHOW__btn__edit']"
# Кнопка "Скрыть стойку"
HIDE_RACK_BUTTON = ("//div[@data-testid='CABINET_SHOW__div__hideCabinet' and "
"contains(@class, 'cabinet_hide_button_trigger_show')]")
# Кнопка "Показать стойку"
SHOW_RACK_BUTTON = ("//div[@data-testid='CABINET_SHOW__div__hideCabinet' and "
"contains(@class, 'cabinet_hide_button_trigger_hide')]")
# Универсальный локатор для любой вкладки по имени # Универсальный локатор для любой вкладки по имени
TAB_BY_NAME = ("//div[starts-with(@data-testid, 'CABINET_SHOW__') and " TAB_BY_NAME = ("//div[starts-with(@data-testid, 'CABINET_SHOW__') and "
"contains(@class, 'v-tabs__div')]//a[contains(@class, 'v-tabs__item') and " "contains(@class, 'v-tabs__div')]//a[contains(@class, 'v-tabs__item') and "
".//*[contains(., '{}')]]") ".//*[contains(., '{}')]]")
# Конкретные вкладки по тексту
COMPOSITION_TAB = ("//div[@data-testid='CABINET_SHOW__composition_tab']"
"//a[contains(@class, 'v-tabs__item')]")
GENERAL_INFO_TAB = ("//div[@data-testid='CABINET_SHOW__main_tab']"
"//a[contains(@class, 'v-tabs__item')]")
MAINTENANCE_TAB = ("//div[@data-testid='CABINET_SHOW__service_tab']"
"//a[contains(@class, 'v-tabs__item')]")
EVENTS_TAB = ("//div[@data-testid='CABINET_SHOW__events_tab']"
"//a[contains(@class, 'v-tabs__item')]")
SERVICES_TAB = ("//div[@data-testid='CABINET_SHOW__services_tab']"
"//a[contains(@class, 'v-tabs__item')]")
# Классы для проверки активности
ACTIVE_TAB_CLASSES = ["accent--text", "v-tabs__item--active"]
# Локатор для активной вкладки # Локатор для активной вкладки
ACTIVE_TAB = ("//div[@data-testid='CABINET_SHOW__tabs']" ACTIVE_TAB = ("//div[@data-testid='CABINET_SHOW__tabs']"
"//a[contains(@class, 'v-tabs__item--active')]") "//a[contains(@class, 'v-tabs__item--active')]")
# Контейнер формы # Контейнер формы создания/редактирования стойки
FORM_CONTAINER = "//div[contains(@class, 'container')]" FORM_INPUT_CONTAINER = "//div[contains(@class, 'flex xs6 pa-0')]"
# Локаторы полей формы редактирования стойки
NAME_FIELD = "//input[@aria-label='Имя']"
SERIAL_NUMBER_FIELD = "//input[@aria-label='Серийный номер']"
INVENTORY_NUMBER_FIELD = "//input[@aria-label='Инвентарный номер']"
CABLE_ENTRY_FIELD = "//input[@aria-label='Ввод кабеля']"
STATUS_FIELD = "//input[@aria-label='Состояние']"
HEIGHT_FIELD = "//input[@aria-label='Высота в юнитах']"
OWNER_FIELD = "//input[@aria-label='Владелец']"
SERVICE_ORG_FIELD = "//input[@aria-label='Обслуживающая организация']"
PROJECT_FIELD = "//input[@aria-label='Проект/Титул']"
# Локаторы полей формы создания стойки # Локаторы полей формы создания стойки
RACK_NAME_FIELD = ("//div[contains(@class, 'container')]" RACK_NAME_FIELD = ("//div[contains(@class, 'container')]"
@ -93,7 +52,6 @@ class RackLocators:
RACK_STATE_FIELD = ("//div[contains(@class, 'container')]" RACK_STATE_FIELD = ("//div[contains(@class, 'container')]"
"//div[contains(@class, 'v-input__slot') and " "//div[contains(@class, 'v-input__slot') and "
".//label[text()='Состояние']]") ".//label[text()='Состояние']]")
RACK_OWNER_FIELD = ("//div[contains(@class, 'container')]" RACK_OWNER_FIELD = ("//div[contains(@class, 'container')]"
"//div[contains(@class, 'v-input__slot') and " "//div[contains(@class, 'v-input__slot') and "
".//label[text()='Владелец']]") ".//label[text()='Владелец']]")
@ -104,21 +62,10 @@ class RackLocators:
"//div[contains(@class, 'v-input__slot') and " "//div[contains(@class, 'v-input__slot') and "
".//label[text()='Проект/Титул']]") ".//label[text()='Проект/Титул']]")
# Универсальные локаторы для поиска combobox полей по имени
COMBOBOX_BY_FIELD_NAME = ('//form[contains(@class, "v-form")]'
'//div[@role="combobox"][.//label[contains(text(), "{}")]]')
COMBOBOX_BY_LABEL = 'form.v-form div[role="combobox"]:has(label:has-text("{}"))'
# Локаторы для выпадающего меню # Локаторы для выпадающего меню
ACTIVE_MENU = 'div.menuable__content__active'
DROPDOWN_LIST = 'div.menuable__content__active div[role="list"]' DROPDOWN_LIST = 'div.menuable__content__active div[role="list"]'
DROPDOWN_ITEM_BY_TEXT = ('div.menuable__content__active ' DROPDOWN_ITEM_BY_TEXT = ('div.menuable__content__active '
'div[role="listitem"]:has(span:has-text("{}"))') 'div[role="listitem"]:has(span:has-text("{}"))')
DROPDOWN_ITEM_XPATH = ('//div[contains(@class, "menuable__content__active")]'
'//div[@role="list"]//div[@role="listitem"][.//*[text()="{}"]]')
# Локатор для родительского контейнера поля ввода
INPUT_PARENT_CONTAINER = "xpath=./ancestor::div[contains(@class, 'v-input')]"
# CSS селекторы для ошибок валидации # CSS селекторы для ошибок валидации
ERROR_CSS_SELECTORS = ".error--text, .v-input--error" ERROR_CSS_SELECTORS = ".error--text, .v-input--error"
@ -138,11 +85,6 @@ class RackLocators:
# Локаторы для определения активной стороны # Локаторы для определения активной стороны
ACTIVE_SIDE_BUTTON = "//button[contains(@class, 'primary--text')]" ACTIVE_SIDE_BUTTON = "//button[contains(@class, 'primary--text')]"
INACTIVE_SIDE_BUTTON = "//button[contains(@class, 'secondary--text')]"
# Для получения текста активной стороны
ACTIVE_SIDE_BUTTON_TEXT = ("//button[contains(@class, 'primary--text')]"
"//div[contains(@class, 'v-btn__content')]")
# Кнопка добавления (add_circle) # Кнопка добавления (add_circle)
ADD_CIRCLE_BUTTON = "//i[contains(text(), 'add_circle')]" ADD_CIRCLE_BUTTON = "//i[contains(text(), 'add_circle')]"
@ -160,3 +102,14 @@ class RackLocators:
# Локатор для слотов в устройствах # Локатор для слотов в устройствах
DEVICE_SLOTS = "//div[contains(@class, 'slot')]" DEVICE_SLOTS = "//div[contains(@class, 'slot')]"
# Кнопка редактирования свойств стойки
EDIT_BUTTON = "//button[@data-testid='CABINET_SHOW__btn__edit']"
# Кнопка "Скрыть стойку"
HIDE_RACK_BUTTON = ("//div[@data-testid='CABINET_SHOW__div__hideCabinet' and "
"contains(@class, 'cabinet_hide_button_trigger_show')]")
# Кнопка "Показать стойку"
SHOW_RACK_BUTTON = ("//div[@data-testid='CABINET_SHOW__div__hideCabinet' and "
"contains(@class, 'cabinet_hide_button_trigger_hide')]")

View File

@ -19,8 +19,8 @@ class SelectionBarLocators:
PARAMETERS_SELECTED = "div.v-select__selections" PARAMETERS_SELECTED = "div.v-select__selections"
# Локаторы для элементов выпадающего списка # Локаторы для элементов выпадающего списка
LISTBOX = "//div[@role='listbox']" LISTBOX = "//div[@role='list']"
LIST_ITEMS = "//div[@role='listbox']//div[@role='listitem']" LIST_ITEMS = "//div[@role='list']//div[@role='listitem']"
# Локатор для родительского контейнера поля ввода # Локатор для родительского контейнера поля ввода
INPUT_PARENT_CONTAINER = "xpath=./ancestor::div[contains(@class, 'v-input')]" INPUT_PARENT_CONTAINER = "xpath=./ancestor::div[contains(@class, 'v-input')]"

View File

@ -4,6 +4,7 @@ import pytest
from playwright.sync_api import Page from playwright.sync_api import Page
from tools.logger import get_logger from tools.logger import get_logger
from locators.navigation_panel_locators import NavigationPanelLocators 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.accounting_objects.rack_maker import RackObjectMaker, RackData
from components_derived.frames.create_child_element_frame import CreateChildElementFrame from components_derived.frames.create_child_element_frame import CreateChildElementFrame
from pages.location_page import LocationPage from pages.location_page import LocationPage
@ -88,6 +89,7 @@ class TestCreateRackElement:
create_child_frame.should_be_toolbar_buttons() create_child_frame.should_be_toolbar_buttons()
#@pytest.mark.develop
def test_create_rack_child_element(self, browser: Page) -> None: def test_create_rack_child_element(self, browser: Page) -> None:
"""Тест создания дочернего элемента типа 'Стойка'.""" """Тест создания дочернего элемента типа 'Стойка'."""
@ -127,6 +129,7 @@ class TestCreateRackElement:
logger.debug("Test for creating 'Rack' child element completed successfully") logger.debug("Test for creating 'Rack' child element completed successfully")
#@pytest.mark.develop
def test_create_rack_with_duplicate_name(self, browser: Page) -> None: def test_create_rack_with_duplicate_name(self, browser: Page) -> None:
""" """
Тест создания стойки с уже существующим именем. Тест создания стойки с уже существующим именем.
@ -150,10 +153,6 @@ class TestCreateRackElement:
# Создаем вторую стойку с тем же именем # Создаем вторую стойку с тем же именем
logger.debug(f"Attempting to create second rack with name '{rack_name}'") logger.debug(f"Attempting to create second rack with name '{rack_name}'")
# Переходим обратно к созданию новой стойки
self.main_page.click_main_navigation_panel_item("test-zone")
self.main_page.wait_for_timeout(2000)
# Нажимаем кнопку "Создать" на тулбаре # Нажимаем кнопку "Создать" на тулбаре
self.location_page.click_create_button() self.location_page.click_create_button()
@ -173,7 +172,7 @@ class TestCreateRackElement:
rack_data = RackData( rack_data = RackData(
name=rack_name, name=rack_name,
height="42", height="42",
depth="1000" depth="450"
) )
# Пытаемся создать вторую стойку с тем же именем # Пытаемся создать вторую стойку с тем же именем
@ -212,53 +211,60 @@ class TestCreateRackElement:
expected_alert_height = test_data["expected_alert_height"] expected_alert_height = test_data["expected_alert_height"]
expected_alert_depth = test_data["expected_alert_depth"] expected_alert_depth = test_data["expected_alert_depth"]
# Получаем контейнер формы
container_locator = create_child_frame.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1)
# Используем метод get_input_fields_locators для получения всех полей
fields_locators = create_child_frame.get_input_fields_locators_(container_locator, "layout")
logger.debug(f"Available fields: {list(fields_locators.keys())}")
# Функция для проверки заполненности combobox поля # Функция для проверки заполненности combobox поля
def is_field_filled(field_name: str) -> bool: def is_field_filled(field_name: str) -> bool:
"""Проверяет, заполнено ли combobox поле.""" """Проверяет, заполнено ли combobox поле."""
# Получаем локатор поля if field_name not in fields_locators:
field_locator = create_child_frame.get_field_locator(field_name) logger.debug(f"Field '{field_name}' not found in fields_locators")
return False
# Находим элемент поля # Получаем xs8 контейнер поля
field_element = create_child_frame.page.locator(field_locator).first field_container = fields_locators[field_name]
if not field_element.is_visible(): if not field_container.is_visible():
logger.debug(f"Field '{field_name}' not visible") logger.debug(f"Field '{field_name}' not visible")
return False return False
# Проверяем наличие кнопки закрытия (крестика) - признак заполненного поля # Проверяем наличие выбранного значения через v-chip (чип выбранного значения в combobox)
close_button = field_element.locator( selected_chip = field_container.locator(".v-chip").first
".v-select__selections" # Или другой локатор для кнопки закрытия
)
# Если есть кнопка закрытия, поле заполнено # Проверяем наличие текста в поле
has_close_button = close_button.count() > 0 and close_button.is_visible() field_text = field_container.text_content() or ""
# Также можно проверить по тексту в поле
field_text = field_element.text_content() or ""
has_text = bool(field_text.strip()) has_text = bool(field_text.strip())
logger.debug(f"Field '{field_name}' - has close button: {has_close_button}, has text: {has_text}") # Проверяем наличие чипа
has_chip = selected_chip.count() > 0 and selected_chip.is_visible()
return has_close_button or has_text logger.debug(f"Field '{field_name}' - has chip: {has_chip}, has text: {has_text}")
return has_chip or has_text
# Проверяем и очищаем поле "Глубина (мм)" только если оно заполнено # Проверяем и очищаем поле "Глубина (мм)" только если оно заполнено
logger.debug("Checking field: Depth (mm)") logger.debug("Checking field: Глубина (мм)")
if is_field_filled("Глубина (мм)"): if is_field_filled("Глубина (мм)"):
logger.debug("Field 'Depth (mm)' is filled, performing clearing") logger.debug("Field 'Глубина (мм)' is filled, performing clearing")
create_child_frame.clear_combobox_field("Глубина (мм)") create_child_frame.clear_combobox_field("Глубина (мм)")
logger.debug("Clearing completed for 'Depth (mm)'") logger.debug("Clearing completed for 'Глубина (мм)'")
else: else:
logger.debug("Field 'Depth (mm)' is already empty, skipping clearing") logger.debug("Field 'Глубина (мм)' is already empty, skipping clearing")
# Проверяем и очищаем поле "Высота в юнитах" только если оно заполнено # Проверяем и очищаем поле "Высота в юнитах" только если оно заполнено
logger.debug("Checking field: Height in units") logger.debug("Checking field: Высота в юнитах")
if is_field_filled("Высота в юнитах"): if is_field_filled("Высота в юнитах"):
logger.debug("Field 'Height in units' is filled, performing clearing") logger.debug("Field 'Высота в юнитах' is filled, performing clearing")
create_child_frame.clear_combobox_field("Высота в юнитах") create_child_frame.clear_combobox_field("Высота в юнитах")
logger.debug("Clearing completed for 'Height in units'") logger.debug("Clearing completed for 'Высота в юнитах'")
else: else:
logger.debug("Field 'Height in units' is already empty, skipping clearing") logger.debug("Field 'Высота в юнитах' is already empty, skipping clearing")
# Создаем объект данных стойки # Создаем объект данных стойки
rack_data = RackData( rack_data = RackData(
@ -274,25 +280,11 @@ class TestCreateRackElement:
# Нажимаем кнопку создания # Нажимаем кнопку создания
logger.debug("Submitting form for validation") logger.debug("Submitting form for validation")
create_child_frame.click_add_button() create_child_frame.click_add_button()
create_child_frame.wait_for_timeout(3000) create_child_frame.wait_for_timeout(1000)
# Проверяем валидацию полей # Проверяем валидацию полей
logger.debug("Checking validation results") logger.debug("Checking validation results")
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")
# Обрабатываем alert-окна # Обрабатываем alert-окна
if not height_value: if not height_value:
logger.debug("Expecting height validation alert") logger.debug("Expecting height validation alert")
@ -306,10 +298,26 @@ class TestCreateRackElement:
create_child_frame.alert.close_alert_by_text(expected_alert_depth) create_child_frame.alert.close_alert_by_text(expected_alert_depth)
logger.debug("Depth alert handled") 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('Создать дочерний элемент в') create_child_frame.check_toolbar_title('Создать дочерний элемент в')
logger.debug("Test completed successfully") logger.debug("Test completed successfully")
@pytest.mark.develop
def test_required_fields_validation(self, browser: Page) -> None: def test_required_fields_validation(self, browser: Page) -> None:
""" """
Тест проверки обязательных полей при создании стойки. Тест проверки обязательных полей при создании стойки.
@ -396,6 +404,49 @@ class TestCreateRackElement:
# Генерируем уникальное имя для финального теста # Генерируем уникальное имя для финального теста
final_rack_name = "Test-Rack-Required-Final" final_rack_name = "Test-Rack-Required-Final"
# **ВАЖНО: Очищаем поля перед заполнением**
logger.debug("Clearing fields before filling...")
# Получаем контейнер формы
container_locator = create_child_frame.page.locator(RackLocators.FORM_INPUT_CONTAINER).nth(1)
# Используем метод get_input_fields_locators для получения всех полей
fields_locators = create_child_frame.get_input_fields_locators_(container_locator, "layout")
# Очищаем поле "Высота в юнитах" если оно заполнено
if "Высота в юнитах" in fields_locators:
field_container = fields_locators["Высота в юнитах"]
# Проверяем наличие текста в поле
field_text = field_container.inner_text() or ""
if field_text.strip():
logger.debug("Clearing 'Высота в юнитах' field...")
create_child_frame.clear_combobox_field("Высота в юнитах")
create_child_frame.wait_for_timeout(500)
# Очищаем поле "Глубина (мм)" если оно заполнено
if "Глубина (мм)" in fields_locators:
field_container = fields_locators["Глубина (мм)"]
# Проверяем наличие текста в поле
field_text = field_container.inner_text() or ""
if field_text.strip():
logger.debug("Clearing 'Глубина (мм)' field...")
create_child_frame.clear_combobox_field("Глубина (мм)")
create_child_frame.wait_for_timeout(500)
# Очищаем поле "Имя" если оно заполнено
if "Имя" in fields_locators:
field_container = fields_locators["Имя"]
# Находим input внутри контейнера
input_field = field_container.locator("input").first
if input_field.count() > 0:
current_value = input_field.input_value()
if current_value.strip():
logger.debug("Clearing 'Имя' field...")
input_field.click()
input_field.press("Control+A")
input_field.press("Backspace")
create_child_frame.wait_for_timeout(500)
# Создаем объект данных стойки # Создаем объект данных стойки
rack_data = RackData( rack_data = RackData(
name=final_rack_name, name=final_rack_name,
@ -414,12 +465,12 @@ class TestCreateRackElement:
# Нажимаем кнопку создания # Нажимаем кнопку создания
create_child_frame.click_add_button() create_child_frame.click_add_button()
create_child_frame.wait_for_timeout(3000) create_child_frame.wait_for_timeout(1000)
# Проверяем, что НЕТ alert-окон для всех обязательных полей # Проверяем, что НЕТ alert-окон для всех обязательных полей
create_child_frame.alert.check_alert_absence(expected_alert_text_height, 1000) #create_child_frame.alert.check_alert_absence(expected_alert_text_height, 1000)
create_child_frame.alert.check_alert_absence(expected_alert_text_depth, 1000) #create_child_frame.alert.check_alert_absence(expected_alert_text_depth, 1000)
logger.debug("No alert windows for required fields appeared - all fields filled correctly") #logger.debug("No alert windows for required fields appeared - all fields filled correctly")
# Проверяем, что ушли со страницы создания # Проверяем, что ушли со страницы создания
try: try: