"""Модуль тестов вкладки 'Стойка'. Содержит тесты для проверки функциональности работы со стойкой оборудования. """ from typing import Optional 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") logger.setLevel("INFO") # Специфичные локаторы оставленые в основном коде 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 (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") # Кнопка "Переместить" replace_button_locator = self.page.locator(RackLocators.TOOLBAR_REPLACE_BUTTON) self.replace_button = TooltipButton(page, replace_button_locator, "replace") # Кнопка "Сохранить" done_button_locator = self.page.locator(RackLocators.TOOLBAR_DONE_BUTTON) self.done_button = TooltipButton(page, done_button_locator, "done") # Кнопка "Отменить" close_button_locator = self.page.locator(RackLocators.TOOLBAR_CLOSE_BUTTON) self.close_button = TooltipButton(page, close_button_locator, "close") # Кнопка "Удалить" remove_button_locator = self.page.locator(RackLocators.TOOLBAR_REMOVE_BUTTON) self.remove_button = TooltipButton(page, remove_button_locator, "remove") 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") 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") # Проверяем тултип кнопки (может быть "Удалить" или "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]: """ Возвращает список доступных вкладок. 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.debug("Total top tab elements found: %d", 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.debug("Top tab found: '%s'", tab_text) logger.debug("Available top tabs found: %s", tabs) return tabs def get_current_active_side(self) -> Optional[str]: """ Возвращает текущую активную сторону стойки. Returns: Optional[str]: "Лицевая сторона", "Обратная сторона" или None если ни одна не активна """ # Проверяем конкретно кнопку "Лицевая сторона" front_btn = self.page.locator(RackLocators.FRONT_SIDE_BUTTON) if front_btn.count() > 0: classes = front_btn.first.get_attribute("class") or "" if "primary--text" in classes.split(): return "Лицевая сторона" # Проверяем конкретно кнопку "Обратная сторона" back_btn = self.page.locator(RackLocators.BACK_SIDE_BUTTON) if back_btn.count() > 0: classes = back_btn.first.get_attribute("class") or "" if "primary--text" in classes.split(): return "Обратная сторона" return None 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_composition_tab(self) -> None: """Переключается на вкладку 'Состав'.""" self.switch_to_tab("Состав") 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 (str): Название вкладки для переключения Raises: AssertionError: Если вкладка не найдена или недоступна """ logger.debug("Switching to tab '%s'...", 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.debug("Tab '%s' is already active", 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 assert target_tab is not None, f"No visible/available tab '{tab_name}' found" # Кликаем на вкладку logger.debug("Clicking on tab '%s'...", 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.debug("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.debug("Side toggle buttons loaded") # Проверяем, что есть активная кнопка active_button = self.page.locator(RackLocators.ACTIVE_SIDE_BUTTON) if active_button.count() > 0: logger.debug("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.debug("Main rack container found (count: %d)", main_container.count()) expect(main_container.first).to_be_attached() # Проверяем наличие позиций юнитов unit_positions = self.page.locator(RackLocators.UNIT_POSITIONS) if unit_positions.count() > 0: logger.debug("Unit positions found: %d", unit_positions.count()) if unit_positions.first.text_content(): content = unit_positions.first.text_content().strip() logger.debug("First position: %s", content) # Проверяем наличие кнопок добавления open_buttons = self.page.locator(RackLocators.ADD_CIRCLE_BUTTON) if open_buttons.count() > 0: logger.debug("'add_circle' buttons found: %d", open_buttons.count()) logger.debug("Rack interface loaded") # Проверки 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.debug( "Devices found: %d (first: ID=%s, Title=%s)", device_count, device_id, device_title ) else: logger.debug("No devices detected") def check_tab_switching(self) -> None: """ Проверяет переключение между вкладками стойки. Raises: AssertionError: Если не удалось переключиться на все вкладки """ logger.debug("Testing rack tab switching functionality...") # Вкладки в правильном порядке defined_tabs = [ "Состав", "Общая информация", "Обслуживание", "События", "Сервисы" ] logger.debug("Defined tabs to test: %s", defined_tabs) successful_switches = 0 failed_switches = [] # Тестируем переключение на каждую определенную вкладку for tab_name in defined_tabs: logger.debug("Testing switch to tab '%s'...", tab_name) try: # Переключаемся на вкладку self.switch_to_tab(tab_name) # Проверяем, что вкладка активна if self.is_tab_active(tab_name): logger.debug("Successfully switched to tab '%s'", tab_name) successful_switches += 1 else: logger.warning("Tab '%s' not active after switching", tab_name) failed_switches.append(f"Tab '{tab_name}' is not active after click") except (AssertionError, TimeoutError) as e: # Ловим только конкретные исключения, которые могут возникнуть при переключении вкладок logger.error("Error switching to tab '%s': %s", tab_name, e) failed_switches.append(f"Tab '{tab_name}' error: {str(e)}") # Небольшая пауза между переключениями self.wait_for_timeout(1000) # Формируем итоговый отчет logger.debug("=== TAB SWITCHING RESULTS ===") logger.debug("Successful switches: %d/%d", successful_switches, len(defined_tabs)) if failed_switches: logger.debug("Failed switches:") for failure in failed_switches: logger.debug(" - %s", 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.debug("All %d defined tabs successfully switched!", successful_switches) def is_tab_active(self, tab_name: str) -> bool: """ Проверяет, активна ли указанная вкладка. Args: tab_name (str): Название вкладки для проверки 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.debug("Tab '%s' is active (via active tab class)", tab_name) return True logger.debug("Tab '%s' is not active", tab_name) return False def should_be_panel_header(self, expected_toolbar_title_items: list[str]) -> None: """ Проверяет наличие и корректность заголовка панели. Args: expected_toolbar_title_items (list[str]): Ожидаемые элементы заголовка 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}" ) def should_be_rack_sides_displayed(self) -> None: """ Проверка отображения и структуры сторон стойки. Raises: AssertionError: Если стороны стойки не отображаются корректно """ logger.debug("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.debug("Side toggle buttons found") # Проверяем, какая сторона активна по умолчанию current_active = self.get_current_active_side() logger.debug("Current active side: %s", 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.debug("Returning to original active side: %s", 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.debug("Final active side: %s", final_active) logger.debug("All rack sides checks passed successfully") def should_be_toolbar_buttons(self) -> None: """ Проверяет наличие и функциональность кнопок тулбара. Raises: AssertionError: Если кнопки недоступны или подсказки неверны """ logger.debug("Checking toolbar buttons...") # Проверяем основные кнопки self.toolbar.check_button_visibility("edit") self.toolbar.check_button_tooltip("edit", "Изменить") # Кликаем на кнопку "Изменить" для проверки функциональности 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: """ Проверка кнопки "Скрыть стойку". Raises: AssertionError: Если кнопка не отображается или не работает """ logger.debug("Checking 'Hide rack' button...") # Проверяем видимость кнопки self.toolbar.check_button_visibility("hide_rack") self.toolbar.check_button_tooltip("hide_rack", "Скрыть стойку") # Получаем общий контейнер стойки до клика rack_container = self.page.locator(RackLocators.RACK_CONTAINER) # Проверяем, что контейнер существует expect(rack_container).to_be_visible(timeout=5000) # Кликаем на кнопку "Скрыть стойку" self.toolbar.get_button_by_name("hide_rack").click() self.wait_for_timeout(2000) # Проверяем, что общий контейнер стойки теперь скрыт (имеет display: none) expect(rack_container).to_have_css("display", "none", timeout=5000) logger.debug("Rack container successfully hidden (display: none)") logger.debug("'Hide rack' button test completed successfully") def should_have_show_rack_button(self) -> None: """ Проверка кнопки "Показать стойку". Raises: AssertionError: Если кнопка не отображается или не работает """ logger.debug("Checking 'Show rack' button...") # Проверяем видимость кнопки self.toolbar.check_button_visibility("show_rack") self.toolbar.check_button_tooltip("show_rack", "Показать стойку") # Получаем общий контейнер стойки rack_container = self.page.locator(RackLocators.RACK_CONTAINER) # Проверяем, что контейнер существует expect(rack_container).to_be_attached(timeout=5000) # Кликаем на кнопку "Показать стойку" self.toolbar.get_button_by_name("show_rack").click() self.wait_for_timeout(2000) # Даем время для применения стилей # Проверяем, что общий контейнер стойки теперь видим (display не равен "none") expect(rack_container).not_to_have_css("display", "none", timeout=5000) logger.debug("Rack container successfully shown (display is not 'none')") logger.debug("'Show rack' button test completed successfully") # Вспомогательные методы def _check_side_details(self, side_name: str, side_button) -> None: """ Проверка структуры конкретной стороны стойки. Args: side_name (str): Название стороны для логов side_button: Локатор кнопки стороны Raises: AssertionError: Если структура стороны некорректна """ logger.debug("Checking %s...", side_name) # Логируем текущее состояние кнопки перед кликом button_classes = side_button.get_attribute("class") or "" logger.debug("Button classes before click: %s", button_classes) # Проверяем, активна ли уже эта сторона current_active = self.get_current_active_side() logger.debug("Current active side (before click): '%s'", current_active) if current_active == side_name: logger.debug("%s is already active", side_name) else: # Если не активна, кликаем для переключения logger.debug("Switching to %s...", side_name) side_button.click() # Даем время на перерисовку классов (увеличиваем время) self.wait_for_timeout(2500) # Проверяем классы после клика button_classes_after = side_button.get_attribute("class") or "" logger.debug("Button classes after click: %s", button_classes_after) # Проверяем, что нужная сторона стала активной active_side = self.get_current_active_side() logger.debug("Active side after switching: '%s'", active_side) assert active_side == side_name, \ f"Wrong side is active: '{active_side}', expected: '{side_name}'" logger.debug("%s successfully activated", side_name) # Проверяем позиции юнитов unit_positions = self.page.locator(RackLocators.UNIT_POSITIONS) total_positions = unit_positions.count() logger.debug("Total unit positions: %d", 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.debug("Units on %s: %d", 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.debug("Devices found: %d (showing first)", device_count) logger.debug(" Device: ID=%s", device_id) logger.debug(" Title: %s", device_title) logger.debug(" Classes: %s", device_classes) logger.debug(" Slots: %d", slot_count) else: logger.debug("No devices detected") 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: """ Ожидает активации вкладки. Args: tab_name (str): Название вкладки для ожидания timeout (int, optional): Время ожидания в миллисекундах, по умолчанию 5000 Raises: AssertionError: Если вкладка не активирована в течение таймаута """ logger.debug("Waiting for tab '%s' activation...", 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.debug("Tab '%s' successfully activated", tab_name) return self.wait_for_timeout(100) assert False, f"Tab '{tab_name}' not activated within {timeout}ms"