From 7f4017024f62357317a7e2dd86683032c5efe20b Mon Sep 17 00:00:00 2001 From: Radislav Date: Tue, 11 Nov 2025 08:40:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B8=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BE=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=BD=D0=B5=D0=B3=D0=BE=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=20=D0=B2=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B5=20?= =?UTF-8?q?=D0=9E=D0=B1=D1=8A=D0=B5=D0=BA=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create_child_element_tab.py | 355 ++++++++++++++++++ .../test_create_child_element.py | 129 +++++++ 2 files changed, 484 insertions(+) create mode 100644 pages/create_elements_tab/create_child_element_tab.py create mode 100644 tests/e2e/create_elements/test_create_child_element.py diff --git a/pages/create_elements_tab/create_child_element_tab.py b/pages/create_elements_tab/create_child_element_tab.py new file mode 100644 index 0000000..178a9bc --- /dev/null +++ b/pages/create_elements_tab/create_child_element_tab.py @@ -0,0 +1,355 @@ +"""Модуль страницы создания дочернего элемента. + +Содержит класс для работы с формой создания дочернего элемента. +""" + +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}' присутствует в списке") diff --git a/tests/e2e/create_elements/test_create_child_element.py b/tests/e2e/create_elements/test_create_child_element.py new file mode 100644 index 0000000..7d322f4 --- /dev/null +++ b/tests/e2e/create_elements/test_create_child_element.py @@ -0,0 +1,129 @@ +"""Модуль тестов создания дочернего элемента в модуле Объекты. + +Содержит тесты для проверки функциональности +создания дочерних элементов оборудования. +""" +import pytest +from playwright.sync_api import Page +from pages.create_elements_tab.create_child_element_tab import CreateChildElementTab +from pages.login_page import LoginPage +from pages.main_page import MainPage +from tools.logger import get_logger + +logger = get_logger("CREATE_CHILD_ELEMENT_TESTS") + + +# @pytest.mark.smoke +class TestCreateChildElement: + """Набор тестов для создания дочернего элемента в модуле Объекты. + + Проверяет корректность отображения и функциональность элементов интерфейса + при создании дочерних элементов оборудования. + + Тесты покрывают следующие функциональные области: + 1. test_child_element_creation_form - Проверка формы создания дочернего элемента + 2. test_object_class_combobox - Проверка combobox 'Класс объекта учета' + """ + + @pytest.fixture(scope="function", autouse=True) + def setup(self, browser: Page) -> None: + """Фикстура для подготовки тестового окружения. + + Выполняет: + 1. Авторизацию в системе + 2. Переход к созданию дочернего элемента через панель навигации: + - Объекты → test-zone → Создать дочерний элемент + + Args: + browser (Page): Экземпляр страницы Playwright для взаимодействия с UI + """ + # Авторизация в системе + login_page = LoginPage(browser) + login_page.do_login() + + # Мы на главной странице + main_page = MainPage(browser) + main_page.should_be_navigation_panel() + main_page.wait_for_timeout(2000) + + # Переходим к Объектам + main_page.click_main_navigation_panel_item("Объекты") + main_page.wait_for_timeout(2000) + + main_page.click_main_navigation_panel_item("test-zone") + main_page.wait_for_timeout(2000) + + # Создаем экземпляр страницы и переходим к созданию дочернего элемента + child_element_page = CreateChildElementTab(browser) + child_element_page.click_create_button() + child_element_page.wait_for_timeout(2000) + + @pytest.mark.develop + def test_child_element_creation_form(self, browser: Page) -> None: + """Тест проверки формы создания дочернего элемента. + + Проверяет: + 1. Корректность заголовка формы создания + 2. Наличие и функциональность кнопки отмены + + Args: + browser (Page): Экземпляр страницы Playwright для взаимодействия с UI + """ + child_element_page = CreateChildElementTab(browser) + + # Проверяем заголовок формы - используем часть текста для надежности + expected_title_part = "Создать дочерний элемент в" + child_element_page.check_toolbar_title(expected_title_part) + + # Проверяем кнопку 'Отменить' в форме создания + child_element_page.should_be_toolbar_buttons() + + child_element_page.wait_for_timeout(2000) + + def test_object_class_combobox(self, browser: Page) -> None: + """Тест проверки combobox 'Класс объекта учета' в форме создания. + + Проверяет: + 1. Наличие combobox на странице + 2. Корректность содержимого + 3. Содержимое списка опций + 4. Возможность выбора каждой опции + + Args: + browser (Page): Экземпляр страницы Playwright для взаимодействия с UI + """ + child_element_page = CreateChildElementTab(browser) + + logger.info("Комплексная проверка combobox 'Класс объекта учета'...") + + # 1. Проверяем наличие combobox + child_element_page.check_object_class_combobox_presence() + + # 2. Проверяем содержимое combobox + child_element_page.check_object_class_combobox_content() + + # 3. Проверяем содержимое списка опций + available_options = child_element_page.get_object_class_options() + + # Проверяем что список не пустой + assert len(available_options) > 0, "Список опций combobox пустой" + + # Проверяем что есть все ожидаемые опции + expected_options = ["Локация", "Стойка", "Устройство", "Модуль"] + missing_options = [opt for opt in expected_options if opt not in available_options] + assert len(missing_options) == 0, f"Отсутствуют опции: {missing_options}. Найдены: {available_options}" + + logger.info(f"Все ожидаемые опции найдены: {available_options}") + + # 4. Проверяем выбор каждой опции по очереди + logger.info("Проверка выбора каждой опции по очереди...") + + for option in expected_options: + logger.info(f"Выбор класса объекта: '{option}'...") + child_element_page.select_object_class(option) + child_element_page.check_object_class_selected(option) + child_element_page.wait_for_timeout(500) + logger.info(f"Класс объекта '{option}' успешно выбран") + + logger.info("Combobox 'Класс объекта учета' прошел все проверки") +