diff --git a/components/toolbar_component.py b/components/toolbar_component.py
index 1e2c4e5..10cc257 100644
--- a/components/toolbar_component.py
+++ b/components/toolbar_component.py
@@ -128,6 +128,41 @@ class ToolbarComponent(BaseComponent):
return title_text
+ def get_toolbar_composite_title_text(self, locator: str|Locator ,
+ timeout: int = 5000) -> []:
+ """Получает составной заголовок тулбара окна в виде списка подзаголовков.
+
+ Args:
+ locator: Локатор для заголовка тулбара
+ separator: Разделитель подзаголовков, по умолчанию галочка вправо (chevron_right)
+ timeout: Таймаут ожидания в миллисекундах
+
+ Returns:
+ str: Текст заголовка тулбара
+
+ Raises:
+ Exception: Если не удалось получить заголовок
+ """
+
+ toolbar_title_items = []
+
+ # Получаем локатор заголовка
+ title_locator = self.get_locator(locator)
+
+ # Ждем появления заголовка с помощью expect
+ expect(title_locator).to_be_visible(timeout=timeout)
+
+ # Найти все элементы
внутри
+ list_items = title_locator.locator("ul > li")
+
+ # Перебрать элементы и получить текст
+ for i in range(list_items.count()):
+ item_text = list_items.nth(i).inner_text()
+ toolbar_title_items.append(item_text)
+
+ return toolbar_title_items
+
+
# Проверки:
def is_button_present(self, name: str) -> bool:
"""Проверяет наличие кнопки.
diff --git a/locators/rack_locators.py b/locators/rack_locators.py
index e059a32..298b979 100644
--- a/locators/rack_locators.py
+++ b/locators/rack_locators.py
@@ -2,6 +2,7 @@
Класс RackLocators хранит XPath локаторы для взаимодействия
с элементами интерфейса стойки оборудования в тестах.
+
"""
class RackLocators:
@@ -21,6 +22,15 @@ class RackLocators:
# Все элементы верхних вкладок стойки
ALL_TABS = "//div[@class='v-tabs__container']//a[contains(@class, 'v-tabs__item')]"
+ # Кнопка редактирования свойств стойки
+ EDIT_BUTTON = "//button[@data-v-5f34ceac and contains(@class, 'font-weight-medium')]"
+
+ # Кнопка "Скрыть стойку"
+ HIDE_RACK_BUTTON = "//div[contains(@class, 'cabinet_hide_button')]//i[contains(@class, 'material-icons') and text()='navigate_before']"
+
+ # Кнопка "Показать стойку"
+ SHOW_RACK_BUTTON = "//div[contains(@class, 'cabinet_hide_button')]//i[contains(@class, 'material-icons') and text()='navigate_next']"
+
# Универсальный локатор для любой вкладки по имени
TAB_BY_NAME = "//div[@class='v-tabs__container']//a[contains(@class, 'v-tabs__item') and contains(., '{}')]"
@@ -69,38 +79,34 @@ class RackLocators:
# CSS селекторы для ошибок валидации
ERROR_CSS_SELECTORS = ".error--text, .v-input--error"
- # Локаторы для отображения сторон стойки
- FRONT_SIDE_CONTAINER = "//div[contains(@class, 'cabinet') and not(contains(@class, 'back'))]"
- BACK_SIDE_CONTAINER = "//div[contains(@class, 'cabinet') and contains(@class, 'back')]"
-
- FRONT_SIDE_TITLE = "//span[contains(@class, 'subheading') and contains(text(), 'Лицевая сторона')]"
- BACK_SIDE_TITLE = "//span[contains(@class, 'subheading') and contains(text(), 'Обратная сторона')]"
-
- # Юниты на лицевой стороне
- FRONT_SIDE_UNITS = ".cabinet:not(.back) .unit, .unit:not(.back)"
-
- # Юниты на обратной стороне
- BACK_SIDE_UNITS = ".cabinet.back .unit"
-
- # Локатор для всех юнитов
- ALL_UNITS = ".unit"
-
- # Устройства на лицевой стороне
- FRONT_SIDE_DEVICES = "//*[contains(@class, 'parent-class')]"
-
- # Устройства на обратной стороне
- BACK_SIDE_DEVICES = "//*[contains(@class, 'parent-class')]"
-
- # Позиции юнитов
- UNIT_POSITIONS = "//div[contains(@class, 'unit-positions')]"
-
- # Контейнер с обеими сторонами
- SIDES_CONTAINER = "//div[contains(@class, 'layout row shrink fill-height ma-0 pa-0')]"
-
- # ЛОКАТОРЫ ДЛЯ СТРУКТУРЫ СТОРОН
- # Основные секции сторон
- FRONT_SIDE_SECTION = "//span[contains(text(), 'Лицевая сторона')]//ancestor::div[contains(@class, 'flex shrink')]"
- BACK_SIDE_SECTION = "//span[contains(text(), 'Обратная сторона')]//ancestor::div[contains(@class, 'flex shrink')]"
+ # ================ ЛОКАТОРЫ ДЛЯ СТРУКТУРЫ СТОЙКИ ===================
# Основной контейнер стойки
- MAIN_CONTAINER = "//div[contains(@class, 'layout row')]"
+ MAIN_CONTAINER = "//div[contains(@class, 'layout cabinet')]"
+
+ # Кнопки переключения сторон
+ FRONT_SIDE_BUTTON = "//button[contains(@class, 'v-btn')]//div[contains(@class, 'v-btn__content') and normalize-space(text())='Лицевая сторона']"
+ BACK_SIDE_BUTTON = "//button[contains(@class, 'v-btn')]//div[contains(@class, 'v-btn__content') and normalize-space(text())='Обратная сторона']"
+
+ # Активная кнопка (имеет класс primary, но НЕ имеет outline)
+ ACTIVE_SIDE_BUTTON = "//button[contains(@class, 'v-btn') and contains(@class, 'primary') and not(contains(@class, 'v-btn--outline'))]"
+ ACTIVE_SIDE_BUTTON_TEXT = "//button[contains(@class, 'v-btn') and contains(@class, 'primary') and not(contains(@class, 'v-btn--outline'))]//div[contains(@class, 'v-btn__content')]"
+
+ # Неактивная кнопка (имеет outline или не имеет primary)
+ INACTIVE_SIDE_BUTTON = "//button[contains(@class, 'v-btn') and (contains(@class, 'v-btn--outline') or not(contains(@class, 'primary')))]"
+
+ # Кнопка добавления (add_circle)
+ ADD_CIRCLE_BUTTON = "//i[contains(text(), 'add_circle')]"
+
+ # Все юниты на стойке
+ ALL_UNITS = "//div[contains(@class, 'unit')]"
+
+ # Позиции юнитов
+ UNIT_POSITIONS = "//div[contains(@class, 'headline') and contains(@class, 'test-xs-center') and contains(@class, 'unit-positions')]"
+
+ # Локатор для устройств
+ DEVICE_ELEMENTS = "//div[contains(@class, 'parent-class')]"
+
+ # Локатор для слотов в устройствах
+ DEVICE_SLOTS = "//div[contains(@class, 'slot')]"
+
diff --git a/pages/rack_page.py b/pages/rack_page.py
new file mode 100644
index 0000000..ada90e3
--- /dev/null
+++ b/pages/rack_page.py
@@ -0,0 +1,585 @@
+"""Модуль тестов вкладки 'Стойка'.
+
+Содержит тесты для проверки функциональности
+работы со стойкой оборудования.
+
+"""
+
+from playwright.sync_api import Page, expect
+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 pages.base_page import BasePage
+
+
+logger = get_logger("RACK_PAGE")
+
+# Специфичные локаторы оставленые в основном коде
+PANEL_HEADER = "//span[text()='Объекты']/following-sibling::i"
+TOOLBAR_CONTENT = "//div[@class='v-toolbar__content']"
+PANEL_HEADER_ANCESTOR_DIV2 = "xpath=/ancestor::div[2]"
+
+
+class RackPage(BasePage):
+ """Класс для работы с вкладкой стойки оборудования."""
+
+ def __init__(self, page: Page) -> None:
+ """
+ Инициализирует объект вкладки стойки.
+
+ Args:
+ page: Экземпляр страницы Playwright
+ """
+
+ super().__init__(page)
+
+ # Кнопка "Изменить"
+ locator_button = self.page.locator(RackLocators.EDIT_BUTTON)
+ self.edit_button = TooltipButton(page, locator_button, "edit")
+
+ # Кнопка "Скрыть стойку"
+ hide_button_locator = self.page.locator(RackLocators.HIDE_RACK_BUTTON)
+ self.hide_button = TooltipButton(page, hide_button_locator, "hide_rack")
+
+ # Кнопка "Показать стойку"
+ show_button_locator = self.page.locator(RackLocators.SHOW_RACK_BUTTON)
+ self.show_button = TooltipButton(page, show_button_locator, "show_rack")
+
+ self.toolbar = ToolbarComponent(page, "")
+ self.toolbar.add_tooltip_button(locator_button, "edit")
+ self.toolbar.add_tooltip_button(hide_button_locator, "hide_rack")
+ self.toolbar.add_tooltip_button(show_button_locator, "show_rack")
+
+ # Действия
+
+ def check_physical_devices_presence(self) -> None:
+ """Проверяет наличие физических устройств на стойке."""
+
+ # Поиск устройств по классу parent-class
+ devices = self.page.locator(RackLocators.DEVICE_ELEMENTS)
+ device_count = devices.count()
+
+ if device_count > 0:
+ # Выводим информацию только о первом устройстве
+ first_device = devices.first
+ device_id = first_device.get_attribute("id") or "No id"
+ device_title = first_device.get_attribute("title") or "No title"
+
+ logger.info(
+ f"Devices found: {device_count} "
+ f"(first: ID={device_id}, Title={device_title})"
+ )
+ else:
+ logger.info("No devices detected")
+
+ def get_available_tabs(self) -> list[str]:
+ """
+ Возвращает список доступных вкладок.
+
+ 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 top tab elements found: {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"Top tab found: '{tab_text}'")
+
+ logger.info(f"Available top tabs found: {tabs}")
+ return tabs
+
+ def get_current_active_side(self) -> str:
+ """
+ Возвращает текущую активную сторону стойки.
+
+ Returns:
+ str: Название активной стороны ('Лицевая сторона' или 'Обратная сторона')
+ """
+
+ active_button = self.page.locator(RackLocators.ACTIVE_SIDE_BUTTON_TEXT)
+ if active_button.count() > 0:
+ text = active_button.first.text_content()
+ if text:
+ return text.strip()
+ return ""
+
+ 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_events_tab(self) -> None:
+ """Переключается на вкладку 'События'."""
+
+ self.switch_to_tab("События")
+
+ 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_services_tab(self) -> None:
+ """Переключается на вкладку 'Сервисы'."""
+
+ self.switch_to_tab("Сервисы")
+
+ def switch_to_tab(self, tab_name: str) -> None:
+ """
+ Переключается на указанную вкладку.
+
+ Args:
+ tab_name: Название вкладки для переключения
+
+ Raises:
+ AssertionError: Если вкладка не найдена или недоступна
+ """
+
+ logger.info(f"Switching to tab '{tab_name}'...")
+
+ tab = self.page.locator(RackLocators.TAB_BY_NAME.format(tab_name))
+
+ assert tab.count() > 0, f"Tab '{tab_name}' not found"
+
+ # Проверяем активность ДО клика
+ if self.is_tab_active(tab_name):
+ logger.info(f"Tab '{tab_name}' is already active")
+ 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
+
+ assert target_tab is not None, f"No visible/available tab '{tab_name}' found"
+
+ # Кликаем на вкладку
+ logger.info(f"Clicking on tab '{tab_name}'...")
+ target_tab.click()
+
+ # Ждем изменения активной вкладки
+ self._wait_for_tab_activation(tab_name)
+
+ # Ждем загрузки контента
+ self.wait_for_timeout(500)
+
+ def wait_for_rack_loading(self) -> None:
+ """Ожидает загрузки интерфейса стойки."""
+
+ logger.info("Waiting for rack interface to load...")
+
+ # Проверяем наличие кнопок переключения сторон
+ front_button = self.page.locator(RackLocators.FRONT_SIDE_BUTTON)
+ back_button = self.page.locator(RackLocators.BACK_SIDE_BUTTON)
+
+ expect(front_button).to_be_visible(timeout=5000)
+ expect(back_button).to_be_visible(timeout=5000)
+ logger.info("Side toggle buttons loaded")
+
+ # Проверяем, что есть активная кнопка
+ active_button = self.page.locator(RackLocators.ACTIVE_SIDE_BUTTON)
+ if active_button.count() > 0:
+ logger.info("Active side button found")
+ else:
+ logger.warning("No active side button found")
+
+ # Проверяем наличие основного контейнера
+ main_container = self.page.locator(RackLocators.MAIN_CONTAINER)
+ if main_container.count() == 0:
+ logger.warning("Main rack container not found")
+ else:
+ logger.info(f"Main rack container found (count: {main_container.count()})")
+ expect(main_container.first).to_be_attached()
+
+ # Проверяем наличие позиций юнитов
+ unit_positions = self.page.locator(RackLocators.UNIT_POSITIONS)
+ if unit_positions.count() > 0:
+ logger.info(f"Unit positions found: {unit_positions.count()}")
+ if unit_positions.first.text_content():
+ content = unit_positions.first.text_content().strip()
+ logger.info(f"First position: {content}")
+
+ # Проверяем наличие кнопок добавления
+ open_buttons = self.page.locator(RackLocators.ADD_CIRCLE_BUTTON)
+ if open_buttons.count() > 0:
+ logger.info(f"'add_circle' buttons found: {open_buttons.count()}")
+
+ logger.info("Rack interface loaded")
+
+ # Проверки
+
+ def check_tab_switching(self) -> None:
+ """
+ Проверяет переключение между вкладками стойки в соответствии с локаторами.
+
+ Raises:
+ AssertionError: Если переключение на одну или более вкладок не удалось
+ """
+
+ logger.info("Testing rack tab switching functionality...")
+
+ # Вкладки
+ defined_tabs = [
+ "Общая информация",
+ "Обслуживание",
+ "События",
+ "Сервисы"
+ ]
+
+ logger.info(f"Defined tabs to test: {defined_tabs}")
+
+ successful_switches = 0
+ failed_switches = []
+
+ # Тестируем переключение на каждую определенную вкладку
+ for tab_name in defined_tabs:
+ logger.info(f"Testing switch to tab '{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 '{tab_name}' not found on page")
+ failed_switches.append(f"Tab '{tab_name}' not found")
+ 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"No visible/available tab '{tab_name}' found")
+ failed_switches.append(f"Tab '{tab_name}' is not visible/available")
+ continue
+
+ # Переключаемся на вкладку
+ logger.info(f"Switching to tab '{tab_name}'...")
+
+ # Проверяем активность ДО клика
+ if self.is_tab_active(tab_name):
+ logger.info(f"Tab '{tab_name}' is already active")
+ 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 '{tab_name}' not active after switching")
+ failed_switches.append(f"Tab '{tab_name}' is not active after click")
+ continue
+
+ logger.info(f"Successfully switched to tab '{tab_name}'")
+ successful_switches += 1
+
+ # Небольшая пауза между переключениями
+ self.wait_for_timeout(1000)
+
+ # Формируем итоговый отчет
+ logger.info("=== TAB SWITCHING RESULTS ===")
+ logger.info(f"Successful switches: {successful_switches}/{len(defined_tabs)}")
+
+ if failed_switches:
+ logger.info("Failed switches:")
+ for failure in failed_switches:
+ logger.info(f" - {failure}")
+
+ # Требуем успешного переключения на все определенные вкладки
+ assert successful_switches == len(defined_tabs), (
+ f"Tab switching test failed. "
+ f"Only {successful_switches} out of {len(defined_tabs)} defined tabs "
+ f"were successfully switched. "
+ f"Errors: {', '.join(failed_switches)}"
+ )
+
+ logger.info(f"All {successful_switches} defined tabs successfully switched!")
+
+ def is_tab_active(self, tab_name: str) -> bool:
+ """
+ Проверяет, активна ли указанная вкладка.
+
+ Args:
+ tab_name: Название вкладки для проверки
+
+ Returns:
+ bool: True если вкладка активна, False в противном случае
+ """
+
+ # Проверяем по активному классу и тексту
+ 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 '{tab_name}' is active (via active tab class)")
+ return True
+
+ logger.info(f"Tab '{tab_name}' is not active")
+ return False
+
+ 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} "
+ f"and expected {expected_toolbar_title_items}"
+ )
+
+ self.toolbar.check_button_visibility("edit")
+
+ def should_be_rack_sides_displayed(self) -> None:
+ """Проверка отображения и структуры сторон стойки."""
+
+ logger.info("Checking rack sides display and structure...")
+
+ # Ожидаем загрузки
+ self.wait_for_rack_loading()
+
+ # БАЗОВАЯ ПРОВЕРКА: наличие кнопок переключения сторон
+
+ front_side_button = self.page.locator(RackLocators.FRONT_SIDE_BUTTON)
+ back_side_button = self.page.locator(RackLocators.BACK_SIDE_BUTTON)
+
+ # Проверяем наличие кнопок
+ expect(front_side_button).to_be_visible(timeout=5000), \
+ "Front side button not found"
+ expect(back_side_button).to_be_visible(timeout=5000), \
+ "Back side button not found"
+
+ logger.info("Side toggle buttons found")
+
+ # Проверяем, какая сторона активна по умолчанию
+ current_active = self.get_current_active_side()
+ logger.info(f"Current active side: {current_active}")
+
+ # Дополнительная проверка устройств
+ self.check_physical_devices_presence()
+
+ # ПРОВЕРКА ЛИЦЕВОЙ СТОРОНЫ
+ self._check_side_details("Лицевая сторона", front_side_button)
+
+ # ПРОВЕРКА ОБРАТНОЙ СТОРОНЫ
+ self._check_side_details("Обратная сторона", back_side_button)
+
+ # Возвращаемся на исходную активную сторону
+ if current_active:
+ logger.info(f"Returning to original active side: {current_active}")
+ if current_active == "Лицевая сторона":
+ front_side_button.click()
+ else:
+ back_side_button.click()
+ self.wait_for_timeout(1000)
+
+ final_active = self.get_current_active_side()
+ logger.info(f"Final active side: {final_active}")
+
+ logger.info("All rack sides checks passed successfully")
+
+ def should_be_toolbar_buttons(self) -> None:
+ """
+ Проверяет наличие и функциональность кнопок тулбара.
+
+ Raises:
+ AssertionError: Если кнопки недоступны или подсказки неверны.
+ """
+
+ logger.info("Checking toolbar buttons...")
+
+ self.toolbar.check_button_visibility("edit")
+ self.toolbar.check_button_tooltip("edit", "Изменить")
+ self.toolbar.get_button_by_name("edit").click()
+
+ def should_have_hide_rack_button(self) -> None:
+ """
+ Упрощенная проверка кнопки "Скрыть стойку".
+ Проверяет только кликабельность и наличие элемента.
+ """
+ logger.info("Checking 'Hide rack' button...")
+
+ # Проверяем видимость
+ self.toolbar.check_button_visibility("hide_rack")
+ self.toolbar.check_button_tooltip("hide_rack", "Скрыть стойку")
+
+ # Проверяем, что кнопка кликабельна
+ self.toolbar.get_button_by_name("hide_rack").click()
+ self.wait_for_timeout(1000)
+
+ def should_have_show_rack_button(self) -> None:
+ """
+ Проверка кнопки "Показать стойку".
+ Проверяет наличие, тултип и кликабельность.
+ """
+
+ logger.info("Checking 'Show rack' button...")
+
+ # Проверяем видимость
+ self.toolbar.check_button_visibility("show_rack")
+ self.toolbar.check_button_tooltip("show_rack", "Показать стойку")
+
+ # Проверяем кликабельность
+ self.toolbar.get_button_by_name("show_rack").click()
+ self.wait_for_timeout(1000)
+
+ # Вспомогательные методы
+
+ def _check_side_details(self, side_name: str, side_button) -> None:
+ """
+ Проверка структуры конкретной стороны стойки.
+
+ Args:
+ side_name: Название стороны для логов
+ side_button: Локатор кнопки стороны
+
+ Raises:
+ AssertionError: Если структура стороны некорректна
+ """
+
+ logger.info(f"Checking {side_name}...")
+
+ # Проверяем, активна ли уже эта сторона
+ current_active = self.get_current_active_side()
+
+ if current_active == side_name:
+ logger.info(f"{side_name} is already active")
+ else:
+ # Если не активна, кликаем для переключения
+ logger.info(f"Switching to {side_name}...")
+ side_button.click()
+ # Даем время на перерисовку
+ self.wait_for_timeout(1500)
+
+ # Проверяем, что сторона активирована
+ active_button_after = self.page.locator(RackLocators.ACTIVE_SIDE_BUTTON_TEXT)
+ assert active_button_after.count() > 0, \
+ f"No active button found after clicking on {side_name}"
+
+ active_text_after = active_button_after.first.text_content().strip()
+ assert active_text_after == side_name, \
+ f"Wrong side is active: '{active_text_after}', expected: '{side_name}'"
+
+ logger.info(f"{side_name} successfully activated")
+
+ # Проверяем позиции юнитов
+ unit_positions = self.page.locator(RackLocators.UNIT_POSITIONS)
+ total_positions = unit_positions.count()
+ logger.info(f"Total unit positions: {total_positions}")
+ assert total_positions > 0, f"No unit positions found on {side_name}"
+
+ # Проверяем юниты
+ all_units = self.page.locator(RackLocators.ALL_UNITS)
+ all_units_count = all_units.count()
+ units_per_side = all_units_count // 2
+ logger.info(f"Units on {side_name}: {units_per_side}")
+
+ # Проверяем устройства
+ devices = self.page.locator(RackLocators.DEVICE_ELEMENTS)
+ device_count = devices.count()
+
+ if device_count > 0:
+ # Выводим информацию только о первом устройстве
+ first_device = devices.first
+ device_id = first_device.get_attribute("id") or "No id"
+ device_title = first_device.get_attribute("title") or "No title"
+ device_classes = first_device.get_attribute("class") or "No classes"
+
+ # Ищем слоты внутри устройства
+ slots = first_device.locator(RackLocators.DEVICE_SLOTS)
+ slot_count = slots.count()
+
+ logger.info(f"Devices found: {device_count} (showing first)")
+ logger.info(f" Device: ID={device_id}")
+ logger.info(f" Title: {device_title}")
+ logger.info(f" Classes: {device_classes}")
+ logger.info(f" Slots: {slot_count}")
+ else:
+ logger.info("No devices detected")
+
+ logger.info(f"{side_name} check completed successfully")
+
+ def _wait_for_tab_activation(self, tab_name: str, timeout: int = 5000) -> None:
+ """
+ Ожидает активации вкладки.
+
+ Args:
+ tab_name: Название вкладки для ожидания
+ timeout: Время ожидания в миллисекундах
+
+ Raises:
+ AssertionError: Если вкладка не активирована в течение таймаута
+ """
+
+ logger.info(f"Waiting for tab '{tab_name}' activation...")
+
+ 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 '{tab_name}' successfully activated")
+ return
+ self.wait_for_timeout(100)
+
+ assert False, f"Tab '{tab_name}' not activated within {timeout}ms"
diff --git a/pages/rack_tab/rack_tab.py b/pages/rack_tab/rack_tab.py
deleted file mode 100644
index 01f722a..0000000
--- a/pages/rack_tab/rack_tab.py
+++ /dev/null
@@ -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} определенных вкладок успешно переключены!")
diff --git a/tests/e2e/elements/test_element_rack.py b/tests/e2e/elements/test_element_rack.py
new file mode 100644
index 0000000..a5fbd8d
--- /dev/null
+++ b/tests/e2e/elements/test_element_rack.py
@@ -0,0 +1,117 @@
+"""Модуль тестов вкладки 'Стойка' в модуле Объекты.
+
+Содержит тесты для проверки функциональности
+работы со стойкой оборудования.
+"""
+
+import pytest
+from playwright.sync_api import Page
+from pages.rack_page import RackPage
+from pages.login_page import LoginPage
+from pages.main_page import MainPage
+
+
+# @pytest.mark.smoke
+class TestRackTab:
+ """Набор тестов для вкладки 'Стойка' в модуле Объекты.
+
+ Проверяет корректность отображения, функциональность элементов интерфейса
+ и переключение между вкладками стойки оборудования.
+
+ Тесты покрывают следующие функциональные области:
+ 1. test_rack_tab_content - Базовая структура и содержимое вкладки стойки
+ 2. test_rack_tab_switching - Функциональность переключения между вкладками стойки
+ """
+
+ @pytest.fixture(scope="function", autouse=True)
+ def setup(self, browser: Page) -> None:
+ """Фикстура для подготовки тестового окружения.
+
+ Выполняет:
+ 1. Авторизацию в системе
+ 2. Переход к стойке оборудования через панель навигации:
+ - Объекты → Физические устройства с опросом → Здание ЦОД 4 → Стойка КСПД
+
+ Args:
+ browser (Page): Экземпляр страницы Playwright для взаимодействия с UI
+ """
+
+ # Авторизация в системе
+ lp = LoginPage(browser)
+ lp.do_login()
+
+ # Мы на главной странице
+ mp = MainPage(browser)
+ mp.should_be_navigation_panel()
+ mp.wait_for_timeout(3000)
+
+ # Переходим к Объектам
+ mp.click_main_navigation_panel_item("Объекты")
+ mp.wait_for_timeout(3000)
+
+ mp.click_subpanel_item("test-zone")
+ mp.wait_for_timeout(3000)
+
+
+ # Переходим к Стойка КСПД с указанием родителя
+ mp.click_subpanel_item("Test-Rack-01", parent="test-zone")
+ mp.wait_for_timeout(3000)
+
+ @pytest.mark.develop
+ def test_rack_tab_content(self, browser: Page) -> None:
+ """Тест содержимого вкладки 'Стойка'.
+
+ Проверяет:
+ 1. Наличие и корректность заголовка панели с навигационной цепочкой
+ 2. Отображение и структуру обеих сторон стойки (лицевой и обратной)
+ 3. Наличие и функциональность кнопок панели инструментов
+ 4. Корректность отображения юнитов и устройств на стойке
+
+ Args:
+ browser (Page): Экземпляр страницы Playwright для взаимодействия с UI
+ """
+
+ expected_toolbar_subtitles = [
+ "test-zone",
+ 'chevron_right',
+ 'Test-Rack-01'
+ ]
+
+ rt = RackPage(browser)
+ rt.should_be_header_panel(expected_toolbar_subtitles)
+
+ # Комплексная проверка отображения обеих сторон стойки с детальной информацией
+ rt.should_be_rack_sides_displayed()
+
+ # Проверка кнопки "Скрыть стойку"
+ rt.should_have_hide_rack_button()
+
+ # Проверка кнопки "Показать стойку"
+ rt.should_have_show_rack_button()
+
+ # Переход в режим редактирования
+ rt.should_be_toolbar_buttons()
+ rt.wait_for_timeout(2000)
+
+ def test_rack_tab_switching(self, browser: Page) -> None:
+ """Тест переключения между вкладками стойки оборудования.
+
+ Проверяет функциональность переключения на все доступные вкладки:
+ 1. Общая информация
+ 2. Обслуживание
+ 3. События
+ 4. Сервисы
+
+ Проверяет:
+ 1. Наличие и доступность всех вкладок
+ 2. Корректность активации вкладок после переключения
+ 3. Отсутствие ошибок при последовательном переключении
+
+ Args:
+ browser (Page): Экземпляр страницы Playwright для взаимодействия с UI
+ """
+
+ rt = RackPage(browser)
+
+ # Проверяем переключение между всеми вкладками стойки
+ rt.check_tab_switching()