Рефакторинг компонентов работы со стойкой
parent
e8f42aa480
commit
4d3731d9f7
Binary file not shown.
Binary file not shown.
|
|
@ -38,7 +38,7 @@ class CreateChildElementFrame(BaseComponent):
|
||||||
has_text="Создать дочерний элемент в"
|
has_text="Создать дочерний элемент в"
|
||||||
).get_by_role("button").nth(0)
|
).get_by_role("button").nth(0)
|
||||||
|
|
||||||
# Кнопка "Отменить" - используем рабочий локатор из старой версии
|
# Кнопка "Отменить" -
|
||||||
cancel_button_locator = self.page.get_by_role("navigation").filter(
|
cancel_button_locator = self.page.get_by_role("navigation").filter(
|
||||||
has_text=re.compile('Создать дочерний элемент в')
|
has_text=re.compile('Создать дочерний элемент в')
|
||||||
).get_by_role("button").nth(1)
|
).get_by_role("button").nth(1)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,465 @@
|
||||||
|
"""Модуль для работы с модальным окном редактирования стойки."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional, Dict
|
||||||
|
from playwright.sync_api import Page, Locator
|
||||||
|
from tools.logger import get_logger
|
||||||
|
from locators.rack_locators import RackLocators
|
||||||
|
from elements.tooltip_button_element import TooltipButton
|
||||||
|
from components.toolbar_component import ToolbarComponent
|
||||||
|
from components.base_component import BaseComponent
|
||||||
|
|
||||||
|
logger = get_logger("MODAL_RACK_EDIT")
|
||||||
|
#logger.setLevel("INFO")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RackEditData:
|
||||||
|
"""Класс для хранения данных редактирования стойки."""
|
||||||
|
|
||||||
|
# Основные поля (редактируемые)
|
||||||
|
name: str = ""
|
||||||
|
serial: str = ""
|
||||||
|
inventory: str = ""
|
||||||
|
comment: str = ""
|
||||||
|
|
||||||
|
# Combobox поля (редактируемые)
|
||||||
|
cable_entry: str = ""
|
||||||
|
state: str = ""
|
||||||
|
owner: str = ""
|
||||||
|
service_org: str = ""
|
||||||
|
project: str = ""
|
||||||
|
|
||||||
|
# Дополнительные поля (редактируемые)
|
||||||
|
power: str = ""
|
||||||
|
|
||||||
|
# Checkbox поля (редактируемые)
|
||||||
|
ventilation_panel: Optional[bool] = None
|
||||||
|
|
||||||
|
# Правила доступа
|
||||||
|
read_access_rules: str = ""
|
||||||
|
write_access_rules: str = ""
|
||||||
|
sms_access_rules: str = ""
|
||||||
|
email_access_rules: str = ""
|
||||||
|
push_access_rules: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class ModalRackEdit(BaseComponent):
|
||||||
|
"""Компонент для работы с модальным окном редактирования стойки."""
|
||||||
|
|
||||||
|
def __init__(self, page: Page) -> None:
|
||||||
|
"""
|
||||||
|
Инициализирует компонент редактирования стойки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page (Page): Экземпляр страницы Playwright
|
||||||
|
"""
|
||||||
|
super().__init__(page)
|
||||||
|
self._form_container = None
|
||||||
|
self._fields_cache = {}
|
||||||
|
self.toolbar = ToolbarComponent(page, "")
|
||||||
|
|
||||||
|
# Кнопка "Переместить" (data-testid)
|
||||||
|
replace_button_locator = self.page.locator(RackLocators.TOOLBAR_REPLACE_BUTTON)
|
||||||
|
self.replace_button = TooltipButton(page, replace_button_locator, "replace")
|
||||||
|
|
||||||
|
# Кнопка "Сохранить" (data-testid)
|
||||||
|
done_button_locator = page.locator(RackLocators.TOOLBAR_DONE_BUTTON)
|
||||||
|
self.done_button = TooltipButton(page, done_button_locator, "done")
|
||||||
|
|
||||||
|
# Кнопка "Отменить" (data-testid)
|
||||||
|
close_button_locator = page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON)
|
||||||
|
self.close_button = TooltipButton(page, close_button_locator, "close")
|
||||||
|
|
||||||
|
# Кнопка "Удалить" (data-testid)
|
||||||
|
remove_button_locator = page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON)
|
||||||
|
self.remove_button = TooltipButton(page, remove_button_locator, "remove")
|
||||||
|
|
||||||
|
# Добавляем кнопки в тулбар
|
||||||
|
self.toolbar.add_tooltip_button(replace_button_locator, "replace")
|
||||||
|
self.toolbar.add_tooltip_button(done_button_locator, "done")
|
||||||
|
self.toolbar.add_tooltip_button(close_button_locator, "close")
|
||||||
|
self.toolbar.add_tooltip_button(remove_button_locator, "remove")
|
||||||
|
|
||||||
|
def click_remove_button(self) -> None:
|
||||||
|
"""
|
||||||
|
Кликает на кнопку 'Удалить' и обрабатывает диалог подтверждения.
|
||||||
|
"""
|
||||||
|
logger.debug("Clicking on 'Remove' button...")
|
||||||
|
|
||||||
|
# Проверяем видимость кнопки
|
||||||
|
self.toolbar.check_button_visibility("remove")
|
||||||
|
self.toolbar.check_button_tooltip("remove", "Удалить")
|
||||||
|
|
||||||
|
# Кликаем на кнопку удаления
|
||||||
|
self.toolbar.get_button_by_name("remove").click()
|
||||||
|
self.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
# Ожидаем появления диалога подтверждения
|
||||||
|
self._handle_remove_confirmation_dialog()
|
||||||
|
|
||||||
|
def click_done_button(self) -> None:
|
||||||
|
"""
|
||||||
|
Кликает на кнопку 'Сохранить' и обрабатывает диалог подтверждения.
|
||||||
|
"""
|
||||||
|
logger.debug("Clicking on 'Done' button...")
|
||||||
|
|
||||||
|
# Проверяем видимость кнопки
|
||||||
|
self.toolbar.check_button_visibility("done")
|
||||||
|
self.toolbar.check_button_tooltip("done", "Сохранить")
|
||||||
|
|
||||||
|
# Кликаем на кнопку удаления
|
||||||
|
self.toolbar.get_button_by_name("done").click()
|
||||||
|
self.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
def confirm_remove_dialog(self, confirm: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Подтверждает или отклоняет удаление в диалоговом окне.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
confirm (bool): Если True - подтвердить удаление, если False - отменить
|
||||||
|
"""
|
||||||
|
logger.debug(f"Confirming remove dialog with: {'Да' if confirm else 'Нет'}")
|
||||||
|
|
||||||
|
# Ждем немного перед поиском диалога
|
||||||
|
self.wait_for_timeout(1500)
|
||||||
|
|
||||||
|
# Ищем активный диалог
|
||||||
|
dialog = self.page.locator("div.v-dialog--active")
|
||||||
|
|
||||||
|
# Проверяем, что диалог найден и содержит нужный текст
|
||||||
|
assert dialog.count() > 0, "No active dialog found"
|
||||||
|
|
||||||
|
# Проверяем текст диалога
|
||||||
|
dialog_text = dialog.first.text_content()
|
||||||
|
logger.debug("Dialog text: %s", dialog_text)
|
||||||
|
|
||||||
|
# Должен содержать "Запрос подтверждения" и "Удалить"
|
||||||
|
assert "Запрос подтверждения" in dialog_text, "Not a confirmation dialog"
|
||||||
|
|
||||||
|
# Ищем кнопку по data-testid
|
||||||
|
if confirm:
|
||||||
|
button = self.page.locator(RackLocators.CONFIRM_REMOVE_YES_BUTTON)
|
||||||
|
else:
|
||||||
|
button = self.page.locator(RackLocators.CONFIRM_REMOVE_NO_BUTTON)
|
||||||
|
|
||||||
|
# Проверяем, что кнопка найдена
|
||||||
|
assert button.count() > 0, "Button not found with selector"
|
||||||
|
|
||||||
|
# Кликаем на кнопку
|
||||||
|
button_text = button.first.text_content()
|
||||||
|
logger.debug("Clicking button with text: %s", button_text)
|
||||||
|
button.first.click()
|
||||||
|
self.wait_for_timeout(2000)
|
||||||
|
|
||||||
|
# Проверяем, что диалог закрылся
|
||||||
|
self.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
logger.debug("Remove confirmation completed")
|
||||||
|
|
||||||
|
def _handle_remove_confirmation_dialog(self) -> None:
|
||||||
|
"""Обрабатывает диалог подтверждения удаления."""
|
||||||
|
logger.debug("Handling remove confirmation dialog...")
|
||||||
|
self.confirm_remove_dialog(confirm=True)
|
||||||
|
|
||||||
|
# Остальные существующие методы остаются без изменений
|
||||||
|
def _get_form_container(self) -> Locator:
|
||||||
|
"""
|
||||||
|
Получает контейнер формы редактирования.
|
||||||
|
"""
|
||||||
|
if self._form_container is None:
|
||||||
|
form_container = self.page.locator("[data-testid='cabinet-bar__cabinet-form']")
|
||||||
|
try:
|
||||||
|
form_container.wait_for(state="visible", timeout=10000)
|
||||||
|
self._form_container = form_container
|
||||||
|
except:
|
||||||
|
raise ValueError("Cabinet form container not found")
|
||||||
|
|
||||||
|
return self._form_container
|
||||||
|
|
||||||
|
def get_available_fields(self) -> list:
|
||||||
|
"""
|
||||||
|
Получает список доступных полей.
|
||||||
|
"""
|
||||||
|
fields_locators = self._get_form_fields()
|
||||||
|
return list(fields_locators.keys()) if fields_locators else []
|
||||||
|
|
||||||
|
def _get_form_fields(self) -> dict:
|
||||||
|
"""
|
||||||
|
Получает все поля формы редактирования стойки.
|
||||||
|
"""
|
||||||
|
if self._fields_cache:
|
||||||
|
return self._fields_cache
|
||||||
|
|
||||||
|
form_container = self._get_form_container()
|
||||||
|
fields_locators = self.get_input_fields_locators(form_container)
|
||||||
|
self._fields_cache = fields_locators
|
||||||
|
|
||||||
|
return fields_locators
|
||||||
|
|
||||||
|
def _fill_text_field(self, field_name: str, value: str) -> bool:
|
||||||
|
"""
|
||||||
|
Заполняет текстовое поле по полному совпадению названия.
|
||||||
|
"""
|
||||||
|
fields_locators = self._get_form_fields()
|
||||||
|
|
||||||
|
# Ищем точное совпадение
|
||||||
|
if field_name not in fields_locators:
|
||||||
|
logger.debug(f"Text field '{field_name}' not found. Available fields: {list(fields_locators.keys())}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
field_container = fields_locators[field_name]
|
||||||
|
|
||||||
|
try:
|
||||||
|
field_container.scroll_into_view_if_needed()
|
||||||
|
self.wait_for_timeout(300)
|
||||||
|
|
||||||
|
# Ищем input поле
|
||||||
|
input_field = field_container.locator("input, textarea").first
|
||||||
|
if input_field.count() == 0:
|
||||||
|
logger.debug(f"Field '{field_name}' doesn't have input element")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Очищаем и заполняем
|
||||||
|
input_field.click()
|
||||||
|
self.wait_for_timeout(200)
|
||||||
|
input_field.fill("")
|
||||||
|
self.wait_for_timeout(200)
|
||||||
|
input_field.fill(value)
|
||||||
|
self.wait_for_timeout(500)
|
||||||
|
|
||||||
|
# Проверяем что значение установлено
|
||||||
|
actual_value = input_field.input_value()
|
||||||
|
if actual_value == value:
|
||||||
|
logger.debug(f"✓ Text field '{field_name}' filled with: '{value}'")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"Field '{field_name}' value mismatch: expected '{value}', got '{actual_value}'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error filling text field '{field_name}': {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _fill_combobox_field(self, field_name: str, value: str) -> bool:
|
||||||
|
"""
|
||||||
|
Заполняет combobox поле по полному совпадению названия.
|
||||||
|
"""
|
||||||
|
fields_locators = self._get_form_fields()
|
||||||
|
|
||||||
|
# Ищем точное совпадение
|
||||||
|
if field_name not in fields_locators:
|
||||||
|
logger.debug(f"Combobox field '{field_name}' not found. Available fields: {list(fields_locators.keys())}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
field_container = fields_locators[field_name]
|
||||||
|
|
||||||
|
try:
|
||||||
|
field_container.scroll_into_view_if_needed()
|
||||||
|
self.wait_for_timeout(300)
|
||||||
|
|
||||||
|
# Ищем кнопку открытия dropdown
|
||||||
|
dropdown_button = field_container.locator(".v-input__append-inner, [role='button']").first
|
||||||
|
|
||||||
|
if dropdown_button.count() == 0:
|
||||||
|
# Может быть поле уже открыто
|
||||||
|
input_field = field_container.locator("input").first
|
||||||
|
input_field.click()
|
||||||
|
self.wait_for_timeout(1000)
|
||||||
|
else:
|
||||||
|
dropdown_button.click()
|
||||||
|
self.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
# Ищем выпадающий список
|
||||||
|
active_menu = None
|
||||||
|
menu_selectors = [
|
||||||
|
".v-menu__content.menuable__content__active",
|
||||||
|
".v-select__menu",
|
||||||
|
".v-autocomplete__content",
|
||||||
|
".v-menu__content"
|
||||||
|
]
|
||||||
|
|
||||||
|
for selector in menu_selectors:
|
||||||
|
menu = self.page.locator(selector).first
|
||||||
|
if menu.count() > 0 and menu.is_visible():
|
||||||
|
active_menu = menu
|
||||||
|
break
|
||||||
|
|
||||||
|
if not active_menu:
|
||||||
|
logger.debug(f"No dropdown menu found for '{field_name}'")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ищем нужный элемент
|
||||||
|
dropdown_item = active_menu.locator(f"div[role='listitem'], .v-list-item").filter(
|
||||||
|
has_text=value
|
||||||
|
).first
|
||||||
|
|
||||||
|
if dropdown_item.count() == 0:
|
||||||
|
logger.debug(f"Value '{value}' not found in dropdown for '{field_name}'")
|
||||||
|
self.page.keyboard.press("Escape")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Выбираем значение
|
||||||
|
dropdown_item.click()
|
||||||
|
logger.debug(f"✓ Combobox '{field_name}' set to: '{value}'")
|
||||||
|
self.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error filling combobox '{field_name}': {e}")
|
||||||
|
self.page.keyboard.press("Escape")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _set_checkbox_field(self, checkbox_label: str, value: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Устанавливает состояние checkbox используя input[type="checkbox"].
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.debug(f"Setting checkbox '{checkbox_label}' to {value}")
|
||||||
|
|
||||||
|
# Ищем все checkbox элементы в форме
|
||||||
|
form_container = self._get_form_container()
|
||||||
|
checkboxes = form_container.locator("input[type='checkbox']")
|
||||||
|
|
||||||
|
if checkboxes.count() == 0:
|
||||||
|
logger.warning("No checkbox elements found in form")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.debug(f"Found {checkboxes.count()} checkbox(es) in form")
|
||||||
|
|
||||||
|
# Если несколько чекбоксов, ищем нужный
|
||||||
|
target_checkbox = None
|
||||||
|
|
||||||
|
# Вариант 1: По data-testid
|
||||||
|
for i in range(checkboxes.count()):
|
||||||
|
checkbox = checkboxes.nth(i)
|
||||||
|
testid = checkbox.get_attribute("data-testid")
|
||||||
|
if testid == "cabinet-bar__main__checkbox__available_ventilation_panel":
|
||||||
|
target_checkbox = checkbox
|
||||||
|
logger.debug(f"Found checkbox by data-testid: {testid}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Вариант 2: По role="checkbox" и aria-checked
|
||||||
|
if target_checkbox is None:
|
||||||
|
for i in range(checkboxes.count()):
|
||||||
|
checkbox = checkboxes.nth(i)
|
||||||
|
role = checkbox.get_attribute("role")
|
||||||
|
if role == "checkbox":
|
||||||
|
target_checkbox = checkbox
|
||||||
|
logger.debug(f"Found checkbox by role='checkbox'")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Вариант 3: Первый найденный checkbox
|
||||||
|
if target_checkbox is None:
|
||||||
|
target_checkbox = checkboxes.first
|
||||||
|
logger.debug("Using first found checkbox")
|
||||||
|
|
||||||
|
# Проверяем состояние
|
||||||
|
current_aria_checked = target_checkbox.get_attribute("aria-checked")
|
||||||
|
is_currently_checked = current_aria_checked == "true"
|
||||||
|
logger.debug(f"Checkbox current state: {is_currently_checked}")
|
||||||
|
|
||||||
|
# Если уже в нужном состоянии
|
||||||
|
if is_currently_checked == value:
|
||||||
|
logger.debug(f"Checkbox already in desired state ({value})")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Кликаем на чекбокс
|
||||||
|
target_checkbox.click(force=True)
|
||||||
|
self.wait_for_timeout(800)
|
||||||
|
|
||||||
|
# Проверяем результат
|
||||||
|
new_aria_checked = target_checkbox.get_attribute("aria-checked")
|
||||||
|
is_now_checked = new_aria_checked == "true"
|
||||||
|
|
||||||
|
if is_now_checked == value:
|
||||||
|
logger.info(f"✓ Checkbox '{checkbox_label}' set to {value}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"Checkbox state didn't change. Still: {is_now_checked}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting checkbox '{checkbox_label}': {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def fill_rack_data(self, rack_data: RackEditData) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Заполняет все доступные поля в форме редактирования.
|
||||||
|
"""
|
||||||
|
logger.debug("Filling rack edit form...")
|
||||||
|
|
||||||
|
results = {
|
||||||
|
"text_fields_filled": 0,
|
||||||
|
"combobox_fields_filled": 0,
|
||||||
|
"checkboxes_set": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Получаем доступные поля
|
||||||
|
available_fields = self.get_available_fields()
|
||||||
|
logger.debug(f"Available fields in form: {available_fields}")
|
||||||
|
|
||||||
|
# 1. Заполняем текстовые поля (только если поле существует)
|
||||||
|
text_fields_mapping = {
|
||||||
|
"Имя": rack_data.name,
|
||||||
|
"Серийный номер": rack_data.serial,
|
||||||
|
"Инвентарный номер": rack_data.inventory,
|
||||||
|
"Комментарий": rack_data.comment,
|
||||||
|
"Выделенная мощность": rack_data.power,
|
||||||
|
"Правила доступа для чтения по умолчанию": rack_data.read_access_rules,
|
||||||
|
"Правила доступа для записи по умолчанию": rack_data.write_access_rules,
|
||||||
|
"Правила доступа по умолчанию для получения СМС": rack_data.sms_access_rules,
|
||||||
|
"Правила доступа по умолчанию для получения email сообщения": rack_data.email_access_rules,
|
||||||
|
"Правила доступа по умолчанию для получения push уведомлений": rack_data.push_access_rules
|
||||||
|
}
|
||||||
|
|
||||||
|
for field_name, value in text_fields_mapping.items():
|
||||||
|
if value and value.strip() and field_name in available_fields:
|
||||||
|
if self._fill_text_field(field_name, value):
|
||||||
|
results["text_fields_filled"] += 1
|
||||||
|
|
||||||
|
# 2. Заполняем combobox поля (только если поле существует)
|
||||||
|
combobox_fields_mapping = {
|
||||||
|
"Ввод кабеля": rack_data.cable_entry,
|
||||||
|
"Состояние": rack_data.state,
|
||||||
|
"Владелец": rack_data.owner,
|
||||||
|
"Обслуживающая организация": rack_data.service_org,
|
||||||
|
"Проект/Титул": rack_data.project
|
||||||
|
}
|
||||||
|
|
||||||
|
for field_name, value in combobox_fields_mapping.items():
|
||||||
|
if value and value.strip() and field_name in available_fields:
|
||||||
|
if self._fill_combobox_field(field_name, value):
|
||||||
|
results["combobox_fields_filled"] += 1
|
||||||
|
|
||||||
|
# 3. Устанавливаем checkbox (если есть)
|
||||||
|
if rack_data.ventilation_panel is not None:
|
||||||
|
if self._set_checkbox_field("Вентиляционная панель", rack_data.ventilation_panel):
|
||||||
|
results["checkboxes_set"] += 1
|
||||||
|
|
||||||
|
logger.debug(f"Fill results: {results}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
def should_be_toolbar_buttons(self) -> None:
|
||||||
|
"""
|
||||||
|
Проверяет наличие и функциональность кнопок тулбара.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: Если кнопки недоступны или подсказки неверны
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.debug("Checking toolbar buttons...")
|
||||||
|
|
||||||
|
# Проверяем новые кнопки тулбара
|
||||||
|
self.toolbar.check_button_visibility("replace")
|
||||||
|
self.toolbar.check_button_tooltip("replace", "Переместить")
|
||||||
|
|
||||||
|
self.toolbar.check_button_visibility("done")
|
||||||
|
self.toolbar.check_button_tooltip("done", "Сохранить")
|
||||||
|
|
||||||
|
self.toolbar.check_button_visibility("close")
|
||||||
|
self.toolbar.check_button_tooltip("close", "Отменить")
|
||||||
|
|
||||||
|
self.toolbar.check_button_visibility("remove")
|
||||||
|
self.toolbar.check_button_tooltip("remove", "Удалить")
|
||||||
|
|
@ -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}' корректно не подсвечено цветом ошибки")
|
|
||||||
|
|
@ -49,102 +49,32 @@ class RackPage(BasePage):
|
||||||
self.show_button = TooltipButton(page, show_button_locator, "show_rack")
|
self.show_button = TooltipButton(page, show_button_locator, "show_rack")
|
||||||
|
|
||||||
# Кнопка "Переместить"
|
# Кнопка "Переместить"
|
||||||
replace_button_locator = self.page.locator(RackLocators.TOOLBAR_REPLACE_BUTTON)
|
# replace_button_locator = self.page.locator(RackLocators.TOOLBAR_REPLACE_BUTTON)
|
||||||
self.replace_button = TooltipButton(page, replace_button_locator, "replace")
|
# self.replace_button = TooltipButton(page, replace_button_locator, "replace")
|
||||||
|
|
||||||
# Кнопка "Сохранить"
|
# Кнопка "Сохранить"
|
||||||
done_button_locator = self.page.locator(RackLocators.TOOLBAR_DONE_BUTTON)
|
# done_button_locator = self.page.locator(RackLocators.TOOLBAR_DONE_BUTTON)
|
||||||
self.done_button = TooltipButton(page, done_button_locator, "done")
|
# self.done_button = TooltipButton(page, done_button_locator, "done")
|
||||||
|
|
||||||
# Кнопка "Отменить"
|
# Кнопка "Отменить"
|
||||||
close_button_locator = self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON)
|
# close_button_locator = self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON)
|
||||||
self.close_button = TooltipButton(page, close_button_locator, "close")
|
# self.close_button = TooltipButton(page, close_button_locator, "close")
|
||||||
|
|
||||||
# Кнопка "Удалить"
|
# Кнопка "Удалить"
|
||||||
remove_button_locator = self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON)
|
# remove_button_locator = self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON)
|
||||||
self.remove_button = TooltipButton(page, remove_button_locator, "remove")
|
# self.remove_button = TooltipButton(page, remove_button_locator, "remove")
|
||||||
|
|
||||||
self.toolbar = ToolbarComponent(page, "")
|
self.toolbar = ToolbarComponent(page, "")
|
||||||
self.toolbar.add_tooltip_button(locator_button, "edit")
|
self.toolbar.add_tooltip_button(locator_button, "edit")
|
||||||
self.toolbar.add_tooltip_button(hide_button_locator, "hide_rack")
|
self.toolbar.add_tooltip_button(hide_button_locator, "hide_rack")
|
||||||
self.toolbar.add_tooltip_button(show_button_locator, "show_rack")
|
self.toolbar.add_tooltip_button(show_button_locator, "show_rack")
|
||||||
self.toolbar.add_tooltip_button(replace_button_locator, "replace")
|
|
||||||
self.toolbar.add_tooltip_button(done_button_locator, "done")
|
#self.toolbar.add_tooltip_button(replace_button_locator, "replace")
|
||||||
self.toolbar.add_tooltip_button(close_button_locator, "close")
|
#self.toolbar.add_tooltip_button(done_button_locator, "done")
|
||||||
self.toolbar.add_tooltip_button(remove_button_locator, "remove")
|
#self.toolbar.add_tooltip_button(close_button_locator, "close")
|
||||||
|
#self.toolbar.add_tooltip_button(remove_button_locator, "remove")
|
||||||
|
|
||||||
# Действия
|
# Действия
|
||||||
|
|
||||||
def click_remove_button(self) -> None:
|
|
||||||
"""
|
|
||||||
Кликает на кнопку 'Удалить' и обрабатывает диалог подтверждения.
|
|
||||||
"""
|
|
||||||
logger.debug("Clicking on 'Remove' button...")
|
|
||||||
|
|
||||||
# Проверяем видимость кнопки
|
|
||||||
self.toolbar.check_button_visibility("remove")
|
|
||||||
|
|
||||||
# Проверяем тултип кнопки (может быть "Удалить" или "Remove")
|
|
||||||
try:
|
|
||||||
self.toolbar.check_button_tooltip("remove", "Удалить")
|
|
||||||
except AssertionError:
|
|
||||||
try:
|
|
||||||
self.toolbar.check_button_tooltip("remove", "Remove")
|
|
||||||
except AssertionError:
|
|
||||||
logger.debug("Could not verify tooltip text for remove button")
|
|
||||||
|
|
||||||
# Кликаем на кнопку удаления
|
|
||||||
self.toolbar.get_button_by_name("remove").click()
|
|
||||||
self.wait_for_timeout(1000)
|
|
||||||
|
|
||||||
# Ожидаем появления диалога подтверждения
|
|
||||||
self._handle_remove_confirmation_dialog()
|
|
||||||
|
|
||||||
def confirm_remove_dialog(self, confirm: bool = True) -> None:
|
|
||||||
"""
|
|
||||||
Подтверждает или отклоняет удаление в диалоговом окне.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
confirm (bool): Если True - подтвердить удаление, если False - отменить
|
|
||||||
"""
|
|
||||||
logger.debug(f"Confirming remove dialog with: {'Да' if confirm else 'Нет'}")
|
|
||||||
|
|
||||||
# Ждем немного перед поиском диалога
|
|
||||||
self.wait_for_timeout(1500)
|
|
||||||
|
|
||||||
# Ищем активный диалог
|
|
||||||
dialog = self.page.locator("div.v-dialog--active")
|
|
||||||
|
|
||||||
# Проверяем, что диалог найден и содержит нужный текст
|
|
||||||
assert dialog.count() > 0, "No active dialog found"
|
|
||||||
|
|
||||||
# Проверяем текст диалога
|
|
||||||
dialog_text = dialog.first.text_content()
|
|
||||||
logger.debug("Dialog text: %s", dialog_text)
|
|
||||||
|
|
||||||
# Должен содержать "Запрос подтверждения" и "Удалить"
|
|
||||||
assert "Запрос подтверждения" in dialog_text, "Not a confirmation dialog"
|
|
||||||
|
|
||||||
# Ищем кнопку по data-testid
|
|
||||||
if confirm:
|
|
||||||
button = self.page.locator(RackLocators.CONFIRM_REMOVE_YES_BUTTON)
|
|
||||||
else:
|
|
||||||
button = self.page.locator(RackLocators.CONFIRM_REMOVE_NO_BUTTON)
|
|
||||||
|
|
||||||
# Проверяем, что кнопка найдена
|
|
||||||
assert button.count() > 0, "Button not found with selector"
|
|
||||||
|
|
||||||
# Кликаем на кнопку
|
|
||||||
button_text = button.first.text_content()
|
|
||||||
logger.debug("Clicking button with text: %s", button_text)
|
|
||||||
button.first.click()
|
|
||||||
self.wait_for_timeout(2000)
|
|
||||||
|
|
||||||
# Проверяем, что диалог закрылся
|
|
||||||
self.wait_for_timeout(1000)
|
|
||||||
|
|
||||||
logger.debug("Remove confirmation completed")
|
|
||||||
|
|
||||||
def get_available_tabs(self) -> list[str]:
|
def get_available_tabs(self) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Возвращает список доступных вкладок.
|
Возвращает список доступных вкладок.
|
||||||
|
|
@ -536,18 +466,6 @@ class RackPage(BasePage):
|
||||||
# Кликаем на кнопку "Изменить" для проверки функциональности
|
# Кликаем на кнопку "Изменить" для проверки функциональности
|
||||||
self.toolbar.get_button_by_name("edit").click()
|
self.toolbar.get_button_by_name("edit").click()
|
||||||
|
|
||||||
# Проверяем новые кнопки тулбара
|
|
||||||
self.toolbar.check_button_visibility("replace")
|
|
||||||
self.toolbar.check_button_tooltip("replace", "Переместить")
|
|
||||||
|
|
||||||
self.toolbar.check_button_visibility("done")
|
|
||||||
self.toolbar.check_button_tooltip("done", "Сохранить")
|
|
||||||
|
|
||||||
self.toolbar.check_button_visibility("close")
|
|
||||||
self.toolbar.check_button_tooltip("close", "Отменить")
|
|
||||||
|
|
||||||
self.toolbar.check_button_visibility("remove")
|
|
||||||
self.toolbar.check_button_tooltip("remove", "Удалить")
|
|
||||||
|
|
||||||
def should_have_hide_rack_button(self) -> None:
|
def should_have_hide_rack_button(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -690,11 +608,6 @@ class RackPage(BasePage):
|
||||||
|
|
||||||
logger.debug("%s check completed successfully", side_name)
|
logger.debug("%s check completed successfully", side_name)
|
||||||
|
|
||||||
def _handle_remove_confirmation_dialog(self) -> None:
|
|
||||||
"""Обрабатывает диалог подтверждения удаления."""
|
|
||||||
logger.debug("Handling remove confirmation dialog...")
|
|
||||||
self.confirm_remove_dialog(confirm=True)
|
|
||||||
|
|
||||||
def _wait_for_tab_activation(self, tab_name: str, timeout: int = 5000) -> None:
|
def _wait_for_tab_activation(self, tab_name: str, timeout: int = 5000) -> None:
|
||||||
"""
|
"""
|
||||||
Ожидает активации вкладки.
|
Ожидает активации вкладки.
|
||||||
|
|
|
||||||
|
|
@ -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} определенных вкладок успешно переключены!")
|
|
||||||
|
|
@ -167,10 +167,15 @@ class TestCreateRackElement:
|
||||||
|
|
||||||
logger.debug("Edit button clicked, waiting for edit form...")
|
logger.debug("Edit button clicked, waiting for edit form...")
|
||||||
|
|
||||||
# 3. Используем метод click_remove_button, который обрабатывает весь процесс удаления
|
# 3. Создаем экземпляр ModalRackEdit и используем его метод
|
||||||
# включая диалог подтверждения
|
rack_edit = ModalRackEdit(browser)
|
||||||
|
|
||||||
|
# Проверяем видимость кнопки удаления в модальном окне
|
||||||
|
rack_edit.check_toolbar_button_visibility("remove")
|
||||||
|
|
||||||
|
# Используем метод из ModalRackEdit для удаления
|
||||||
logger.debug("Clicking remove button...")
|
logger.debug("Clicking remove button...")
|
||||||
rack_page.click_remove_button()
|
rack_edit.click_remove_button()
|
||||||
|
|
||||||
# 4. Проверяем уведомление об успешном удалении - требуется создать разработчику (заведена задача)
|
# 4. Проверяем уведомление об успешном удалении - требуется создать разработчику (заведена задача)
|
||||||
# Создаем экземпляр фрейма для доступа к alert компоненту
|
# Создаем экземпляр фрейма для доступа к alert компоненту
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,16 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from playwright.sync_api import Page
|
from playwright.sync_api import Page, expect
|
||||||
from locators.navigation_panel_locators import NavigationPanelLocators
|
from locators.navigation_panel_locators import NavigationPanelLocators
|
||||||
|
from pages.location_page import LocationPage
|
||||||
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.login_page import LoginPage
|
from pages.login_page import LoginPage
|
||||||
from pages.main_page import MainPage
|
from pages.main_page import MainPage
|
||||||
from pages.rack_page import RackPage
|
from pages.rack_page import RackPage
|
||||||
|
from tools.logger import get_logger
|
||||||
|
from components_derived.modal_rack_edit import ModalRackEdit, RackEditData
|
||||||
|
|
||||||
# Константы
|
# Константы
|
||||||
RACK_NAME = "Test-Rack-Functionality"
|
RACK_NAME = "Test-Rack-Functionality"
|
||||||
|
|
@ -118,9 +120,28 @@ class TestRackTab:
|
||||||
# Кликаем на кнопку "Изменить"
|
# Кликаем на кнопку "Изменить"
|
||||||
rack_page.toolbar.get_button_by_name("edit").click()
|
rack_page.toolbar.get_button_by_name("edit").click()
|
||||||
|
|
||||||
# 3. Используем метод click_remove_button, который обрабатывает весь процесс удаления
|
# 3. Создаем экземпляр ModalRackEdit
|
||||||
# включая диалог подтверждения
|
rack_edit = ModalRackEdit(browser)
|
||||||
rack_page.click_remove_button()
|
|
||||||
|
# Используем метод для удаления
|
||||||
|
rack_edit.click_remove_button()
|
||||||
|
|
||||||
|
# 4. Проверяем уведомление об успешном удалении - требуется создать разработчику (заведена задача)
|
||||||
|
# Создаем экземпляр фрейма для доступа к alert компоненту
|
||||||
|
# create_child_frame = CreateChildElementFrame(browser)
|
||||||
|
|
||||||
|
# Проверяем наличие любого alert-окна (не обязательно точного текста)
|
||||||
|
# create_child_frame.alert.check_alert_presence("")
|
||||||
|
|
||||||
|
# Получаем текст alert, чтобы убедиться что удаление прошло успешно
|
||||||
|
# alert_text = create_child_frame.alert.get_text()
|
||||||
|
# logger.debug(f"Alert text after deletion: {alert_text}")
|
||||||
|
|
||||||
|
# Проверяем что в тексте есть указание на успешное удаление
|
||||||
|
# assert "удален" in alert_text.lower() or "успешно" in alert_text.lower()
|
||||||
|
|
||||||
|
# Закрываем alert
|
||||||
|
# create_child_frame.alert.close_alert()
|
||||||
|
|
||||||
@pytest.fixture(scope="function", autouse=True)
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
def setup(self, browser: Page) -> None:
|
def setup(self, browser: Page) -> None:
|
||||||
|
|
@ -251,3 +272,107 @@ class TestRackTab:
|
||||||
# Переход в режим редактирования
|
# Переход в режим редактирования
|
||||||
rt.should_be_toolbar_buttons()
|
rt.should_be_toolbar_buttons()
|
||||||
rt.wait_for_timeout(1000)
|
rt.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
@pytest.mark.develop
|
||||||
|
def test_rack_general_info_tab_fields(self, browser: Page) -> None:
|
||||||
|
"""Тест заполнения полей вкладки 'Общая информация' стойки."""
|
||||||
|
logger = get_logger("RACK_GENERAL_INFO_TEST")
|
||||||
|
|
||||||
|
rt = RackPage(browser)
|
||||||
|
|
||||||
|
# Переходим в режим редактирования
|
||||||
|
logger.debug("Switching to edit mode...")
|
||||||
|
rt.toolbar.check_button_visibility("edit")
|
||||||
|
rt.toolbar.get_button_by_name("edit").click()
|
||||||
|
rt.wait_for_timeout(3000)
|
||||||
|
|
||||||
|
# Создаем экземпляр ModalRackEdit
|
||||||
|
rack_edit = ModalRackEdit(browser)
|
||||||
|
rack_edit.should_be_toolbar_buttons()
|
||||||
|
|
||||||
|
# Получаем список доступных полей (используем точные названия из этого списка)
|
||||||
|
available_fields = rack_edit.get_available_fields()
|
||||||
|
logger.info(f"Available fields in form: {available_fields}")
|
||||||
|
|
||||||
|
# Создаем маппинг: используем ТОЧНЫЕ названия полей из available_fields
|
||||||
|
field_mapping = {}
|
||||||
|
|
||||||
|
# Текстовые поля
|
||||||
|
for field_pattern, test_value in [
|
||||||
|
("Имя", "Test-Rack-Functionality"),
|
||||||
|
("Серийный номер", "SN123456789"),
|
||||||
|
("Инвентарный номер", "INV987654321"),
|
||||||
|
("Комментарий", "Тестовый комментарий для стойки"),
|
||||||
|
("Выделенная мощность (Вт/ВА)", "55"),
|
||||||
|
]:
|
||||||
|
# Ищем точное совпадение
|
||||||
|
if field_pattern in available_fields:
|
||||||
|
field_mapping[field_pattern] = ("text", test_value)
|
||||||
|
|
||||||
|
# Combobox поля
|
||||||
|
for field_pattern, test_value in [
|
||||||
|
("Ввод кабеля", "Сверху"),
|
||||||
|
("Состояние", "Введен в эксплуатацию"),
|
||||||
|
("Владелец", "Тестовый владелец"),
|
||||||
|
("Обслуживающая организация", "Тестовая сервисная организация"),
|
||||||
|
("Проект/Титул", "Тестовый проект"),
|
||||||
|
]:
|
||||||
|
if field_pattern in available_fields:
|
||||||
|
field_mapping[field_pattern] = ("combobox", test_value)
|
||||||
|
|
||||||
|
# Заполняем каждое поле вручную
|
||||||
|
results = {
|
||||||
|
"text_fields_filled": 0,
|
||||||
|
"combobox_fields_filled": 0,
|
||||||
|
"checkboxes_set": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Filling fields individually...")
|
||||||
|
|
||||||
|
for field_name, (field_type, value) in field_mapping.items():
|
||||||
|
logger.info(f"Filling {field_type} field '{field_name}' with '{value}'...")
|
||||||
|
|
||||||
|
if field_type == "text":
|
||||||
|
success = rack_edit._fill_text_field(field_name, value)
|
||||||
|
if success:
|
||||||
|
results["text_fields_filled"] += 1
|
||||||
|
logger.info(f"✓ Text field '{field_name}' filled")
|
||||||
|
else:
|
||||||
|
logger.warning(f"✗ Failed to fill text field '{field_name}'")
|
||||||
|
|
||||||
|
elif field_type == "combobox":
|
||||||
|
success = rack_edit._fill_combobox_field(field_name, value)
|
||||||
|
if success:
|
||||||
|
results["combobox_fields_filled"] += 1
|
||||||
|
logger.info(f"✓ Combobox field '{field_name}' filled")
|
||||||
|
else:
|
||||||
|
logger.warning(f"✗ Failed to fill combobox field '{field_name}'")
|
||||||
|
|
||||||
|
# Устанавливаем checkbox
|
||||||
|
test_ventilation_panel = True
|
||||||
|
logger.info("Setting ventilation panel checkbox...")
|
||||||
|
success = rack_edit._set_checkbox_field("Вентиляционная панель", test_ventilation_panel)
|
||||||
|
if success:
|
||||||
|
results["checkboxes_set"] += 1
|
||||||
|
logger.info("✓ Checkbox set")
|
||||||
|
else:
|
||||||
|
logger.warning("✗ Failed to set checkbox")
|
||||||
|
|
||||||
|
# Проверяем результаты
|
||||||
|
logger.info(f"Fill results: {results}")
|
||||||
|
|
||||||
|
# Проверяем что хотя бы некоторые поля были заполнены
|
||||||
|
total_filled = results.get("text_fields_filled", 0) + results.get("combobox_fields_filled", 0)
|
||||||
|
assert total_filled > 0, f"No fields were filled successfully. Results: {results}"
|
||||||
|
|
||||||
|
# Сохраняем изменения
|
||||||
|
logger.info("Saving changes...")
|
||||||
|
# Используем метод из ModalRackEdit для кнопки "Сохранить"
|
||||||
|
rack_edit.click_done_button()
|
||||||
|
rack_edit.wait_for_timeout(3000)
|
||||||
|
|
||||||
|
# Проверяем выход из режима редактирования
|
||||||
|
rt.toolbar.check_button_visibility("edit")
|
||||||
|
logger.info("✓ Successfully exited edit mode")
|
||||||
|
|
||||||
|
logger.info("✓ General Info tab fields test completed")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue