From 35401be507131a3f5acf336192768b427457d0c2 Mon Sep 17 00:00:00 2001 From: Radislav Date: Mon, 1 Dec 2025 08:53:19 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=84=D1=80?= =?UTF-8?q?=D0=B5=D0=B9=D0=BC=D0=B0=20CreateChildElementFrame=20=D0=B2=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/navbar_component.py | 4 + pages/location_page.py | 75 ++--- .../test_create_rack_element.py | 279 ++++++++---------- tests/e2e/test_expand_navigation_panel.py | 2 +- tools/__pycache__/__init__.cpython-313.pyc | Bin 312 -> 0 bytes .../fix_python_project.cpython-313.pyc | Bin 8095 -> 0 bytes tools/__pycache__/logger.cpython-313.pyc | Bin 1639 -> 0 bytes 7 files changed, 178 insertions(+), 182 deletions(-) delete mode 100644 tools/__pycache__/__init__.cpython-313.pyc delete mode 100644 tools/__pycache__/fix_python_project.cpython-313.pyc delete mode 100644 tools/__pycache__/logger.cpython-313.pyc diff --git a/components/navbar_component.py b/components/navbar_component.py index 4a8b318..d39c2f8 100644 --- a/components/navbar_component.py +++ b/components/navbar_component.py @@ -1,5 +1,6 @@ """Модуль компонента панели навигации. Содержит класс для работы с элементами навигации.""" +import re from playwright.sync_api import Page, Locator from tools.logger import get_logger from locators.navigation_panel_locators import NavigationPanelLocators @@ -51,6 +52,9 @@ class NavigationPanelComponent(BaseComponent): node_content = node.locator('div.v-treeview-node__content') if node_content.count() > 0: node_text = node_content.first.inner_text().strip() + node_texts = node_text.splitlines() + if len(node_texts) > 1: + node_text = node_texts[1] if item_name == node_text: node_attr = node.get_attribute('class') if "v-treeview-node--leaf" not in node_attr: diff --git a/pages/location_page.py b/pages/location_page.py index 066a3cc..de5ca4f 100644 --- a/pages/location_page.py +++ b/pages/location_page.py @@ -2,21 +2,20 @@ from playwright.sync_api import Page from components.toolbar_component import ToolbarComponent -from components_derived.frames.create_child_element_frame import ( - CreateChildElementFrame -) +from components_derived.frames.create_child_element_frame import CreateChildElementFrame from pages.base_page import BasePage -# =============== Локаторы ================================================ -PANEL_HEADER = "//span[text()='Объекты']/following-sibling::i" -CREATE_BUTTON_ANCESTOR_DIV3 = "xpath=/ancestor::div[3]//button" -# ========================================================================= - - class LocationPage(BasePage): """Класс для работы со страницей локации.""" + # Константы локаторов + TOOLBAR_BUTTONS_LOCATOR = "//div[contains(@class, 'layout class--')]//span[@class='v-tooltip v-tooltip--bottom']//button" + + # Индексы кнопок + CREATE_BUTTON_INDEX = 0 # Первая кнопка + EDIT_BUTTON_INDEX = 1 # Вторая кнопка + def __init__(self, page: Page) -> None: """ Инициализирует страницу локации. @@ -29,49 +28,59 @@ class LocationPage(BasePage): # Инициализация тулбара self.toolbar = ToolbarComponent(page, "") - panel_header_locator = self.page.locator(PANEL_HEADER) + # Кнопка "Создать" - первая кнопка + create_button_locator = self.page.locator( + self.TOOLBAR_BUTTONS_LOCATOR + ).nth(self.CREATE_BUTTON_INDEX) - # Кнопка "Создать" - первая кнопка в тулбаре - create_button_locator = panel_header_locator.locator( - CREATE_BUTTON_ANCESTOR_DIV3 - ).nth(0) + # Кнопка "Изменить" - вторая кнопка + edit_button_locator = self.page.locator( + self.TOOLBAR_BUTTONS_LOCATOR + ).nth(self.EDIT_BUTTON_INDEX) - # Инициализация кнопки + # Инициализация кнопок self.toolbar.add_tooltip_button(create_button_locator, "create") + self.toolbar.add_tooltip_button(edit_button_locator, "edit") - # Инициализация фреймов (ленивая загрузка) self._create_child_frame = None - def click_create_button(self) -> CreateChildElementFrame: + def click_create_button(self) -> None: """ - Кликает на кнопку 'Создать' и возвращает фрейм создания. + Кликает на кнопку 'Создать'. Returns: - CreateChildElementFrame: Фрейм создания дочернего элемента + None """ - # Используем метод тулбара для клика self.toolbar.click_button("create") - self.wait_for_timeout(3000) - # Создаем и возвращаем фрейм - self._create_child_frame = CreateChildElementFrame(self.page) - return self._create_child_frame - - def is_create_button_visible(self) -> bool: + def click_edit_button(self) -> None: """ - Проверяет видимость кнопки 'Создать'. + Кликает на кнопку 'Изменить'. Returns: - bool: True если кнопка видима + None """ - button = self.toolbar.get_button_by_name("create") + self.toolbar.click_button("edit") + self.wait_for_timeout(2000) - if button is None: - return False + def should_be_toolbar_buttons(self) -> None: + """ + Проверяет наличие и функциональность кнопок тулбара. - return button.is_present(timeout=5000) and button.locator.is_visible() + Raises: + AssertionError: Если кнопки недоступны или подсказки неверны. + """ + # Проверяем кнопку "Создать" + self.toolbar.check_button_visibility("create") + self.toolbar.check_button_tooltip("create", "Создать") + + # Проверяем кнопку "Изменить" + self.toolbar.check_button_visibility("edit") + self.toolbar.check_button_tooltip("edit", "Изменить") + + self.wait_for_timeout(2000) def wait_for_timeout(self, timeout: int) -> None: """ @@ -80,4 +89,4 @@ class LocationPage(BasePage): Args: timeout: Время ожидания в миллисекундах """ - self.page.wait_for_timeout(timeout) \ No newline at end of file + self.page.wait_for_timeout(timeout) diff --git a/tests/e2e/create_elements/test_create_rack_element.py b/tests/e2e/create_elements/test_create_rack_element.py index 8449bd7..01965e3 100644 --- a/tests/e2e/create_elements/test_create_rack_element.py +++ b/tests/e2e/create_elements/test_create_rack_element.py @@ -50,15 +50,18 @@ class TestCreateRackElement: # Создаем экземпляр страницы локации self.location_page = LocationPage(browser) - #@pytest.mark.develop + @pytest.mark.develop def test_create_rack_content(self, browser: Page) -> None: """Тест создания дочернего элемента типа 'Стойка'.""" # Проверяем что кнопка "Создать" доступна - assert self.location_page.is_create_button_visible(), "Create button is not visible on the page" + self.location_page.should_be_toolbar_buttons() # Нажимаем кнопку "Создать" на тулбаре - create_child_frame = self.location_page.click_create_button() + self.location_page.click_create_button() + + # Создаем фрейм создания дочернего элемента + create_child_frame = CreateChildElementFrame(browser) # Нажимаем на плашку "Класс объекта учета" create_child_frame.open_object_class_combobox() @@ -82,7 +85,10 @@ class TestCreateRackElement: """Тест создания дочернего элемента типа 'Стойка'.""" # Нажимаем кнопку "Создать" на тулбаре - create_child_frame = self.location_page.click_create_button() + self.location_page.click_create_button() + + # Создаем фрейм создания дочернего элемента + create_child_frame = CreateChildElementFrame(browser) # Нажимаем на плашку "Класс объекта учета" create_child_frame.open_object_class_combobox() @@ -131,7 +137,7 @@ class TestCreateRackElement: # Проверяем, существует ли уже стойка с таким именем if not self._check_rack_exists(browser, rack_name): logger.info(f"Rack with name '{rack_name}' not found. Creating first rack.") - create_child_frame = self._create_rack(browser, rack_name) + self._create_rack(browser, rack_name) logger.info(f"First rack with name '{rack_name}' created successfully") else: logger.info(f"Rack with name '{rack_name}' already exists, proceeding to create second one") @@ -144,7 +150,10 @@ class TestCreateRackElement: self.main_page.wait_for_timeout(2000) # Нажимаем кнопку "Создать" на тулбаре - create_child_frame = self.location_page.click_create_button() + self.location_page.click_create_button() + + # Создаем фрейм создания дочернего элемента + create_child_frame = CreateChildElementFrame(browser) # Нажимаем на плашку "Класс объекта учета" create_child_frame.open_object_class_combobox() @@ -179,23 +188,76 @@ class TestCreateRackElement: logger.info("System prevented creating rack with duplicate name") + def _perform_required_fields_test(self, create_child_frame, rack_maker, test_data): + """Выполняет один тест валидации обязательных полей. + + Args: + create_child_frame: Фрейм создания дочернего элемента + rack_maker: Объект для работы со стойкой + test_data: Словарь с данными теста + """ + # Распаковываем данные теста + name_value = test_data["name"] + height_value = test_data["height"] + depth_value = test_data["depth"] + expected_alert_height = test_data["expected_alert_height"] + expected_alert_depth = test_data["expected_alert_depth"] + + # Очистить поля + create_child_frame.clear_combobox_field("Глубина (мм)") + create_child_frame.clear_combobox_field("Высота в юнитах") + + # Заполняем данные + rack_maker.fill_rack_data( + name=name_value, + height=height_value, + depth=depth_value + ) + + # Нажимаем кнопку создания + create_child_frame.click_add_button() + create_child_frame.wait_for_timeout(3000) + + # Проверяем подсветку полей + if height_value: + create_child_frame.check_field_not_highlighted_error("Высота в юнитах") + else: + create_child_frame.check_field_highlighted_error("Высота в юнитах") + + if depth_value: + create_child_frame.check_field_not_highlighted_error("Глубина (мм)") + else: + create_child_frame.check_field_highlighted_error("Глубина (мм)") + + # Обрабатываем alert-окна + if not height_value: + create_child_frame.alert.check_alert_presence(expected_alert_height) + create_child_frame.alert.close_alert_by_text(expected_alert_height) + + if not depth_value: + create_child_frame.alert.check_alert_presence(expected_alert_depth) + create_child_frame.alert.close_alert_by_text(expected_alert_depth) + + # Проверяем, что остались на той же странице + create_child_frame.check_toolbar_title('Создать дочерний элемент в') + def test_required_fields_validation(self, browser: Page) -> None: """ Тест проверки обязательных полей при создании стойки. Проверяет, что система корректно валидирует обязательные поля: - - Поле 'Имя' должно быть заполнено - Поле 'Высота в юнитах' должно быть заполнено - Поле 'Глубина (мм)' должно быть заполнено """ - # Текст сообщения alert-окна - expected_alert_text_name = "поле Имя должно быть заполнено" expected_alert_text_height = "поле Высота в юнитах должно быть заполнено" expected_alert_text_depth = "поле Глубина (мм) должно быть заполнено" # Нажимаем кнопку "Создать" на тулбаре - create_child_frame = self.location_page.click_create_button() + self.location_page.click_create_button() + + # Создаем фрейм создания дочернего элемента + create_child_frame = CreateChildElementFrame(browser) # Нажимаем на плашку "Класс объекта учета" create_child_frame.open_object_class_combobox() @@ -209,136 +271,57 @@ class TestCreateRackElement: # Проверяем наличие полей стойки rack_maker.check_rack_fields_presence() - # 1. Тест: Попытка создания стойки поля - default - logger.info("Test 1: Creating rack with default field values") + # Тестовые данные + test_cases = [ + { + "name": "Test 1: Creating rack with default field values", + "data": { + "name": "", + "height": "", + "depth": "", + "expected_alert_height": expected_alert_text_height, + "expected_alert_depth": expected_alert_text_depth + } + }, + { + "name": "Test 2: Required fields are not filled", + "data": { + "name": "", + "height": "", + "depth": "", + "expected_alert_height": expected_alert_text_height, + "expected_alert_depth": expected_alert_text_depth + } + }, + { + "name": "Test 3: Only 'Height in units' field is filled", + "data": { + "name": "", + "height": "42", + "depth": "", + "expected_alert_height": expected_alert_text_height, + "expected_alert_depth": expected_alert_text_depth + } + }, + { + "name": "Test 4: Only 'Depth (mm)' field is filled", + "data": { + "name": "", + "height": "", + "depth": "1000", + "expected_alert_height": expected_alert_text_height, + "expected_alert_depth": expected_alert_text_depth + } + } + ] - # Нажимаем кнопку создания без заполнения данных - create_child_frame.click_add_button() - create_child_frame.wait_for_timeout(2000) - - # Проверяем подсветку обязательных полей цветом ошибки - create_child_frame.check_field_highlighted_error("Высота в юнитах") - create_child_frame.check_field_highlighted_error("Глубина (мм)") - - logger.info("Validation for 'Name' field temporarily disabled - waiting for developer fix") - - # Проверяем наличие alert-окна с сообщением о заполнении Высота в юнитах - create_child_frame.alert.check_alert_presence(expected_alert_text_height) - # Закрываем alert-окно Высота в юнитах - create_child_frame.alert.close_alert_by_text(expected_alert_text_height) - - # Проверяем наличие alert-окна с сообщением о заполнении Глубины (мм) - create_child_frame.alert.check_alert_presence(expected_alert_text_depth) - # Закрываем alert-окно Глубины (мм) - create_child_frame.alert.close_alert_by_text(expected_alert_text_depth) - - # Проверяем, что остались на той же странице - create_child_frame.check_toolbar_title('Создать дочерний элемент в') - logger.info("System prevented creating rack without height and depth") - create_child_frame.wait_for_timeout(2000) - - # 2. Тест: Обязательные поля не заполнены - logger.info("Test 2: Required fields are not filled") - - rack_maker.fill_rack_data( - name="", - height="", - depth="" - ) - - # Нажимаем кнопку создания без заполнения данных - create_child_frame.click_add_button() - create_child_frame.wait_for_timeout(2000) - - # Проверяем подсветку всех обязательных полей цветом ошибки - create_child_frame.check_field_highlighted_error("Высота в юнитах") - create_child_frame.check_field_highlighted_error("Глубина (мм)") - - logger.info("Validation for 'Name' field temporarily disabled - waiting for developer fix") - - # Проверяем наличие alert-окна с сообщением о заполнении Высота в юнитах - create_child_frame.alert.check_alert_presence(expected_alert_text_height) - # Закрываем alert-окно Высота в юнитах - create_child_frame.alert.close_alert_by_text(expected_alert_text_height) - - # Проверяем наличие alert-окна с сообщением о заполнении Глубины (мм) - create_child_frame.alert.check_alert_presence(expected_alert_text_depth) - # Закрываем alert-окно Глубины (мм) - create_child_frame.alert.close_alert_by_text(expected_alert_text_depth) - - # Проверяем, что остались на той же странице - create_child_frame.check_toolbar_title('Создать дочерний элемент в') - logger.info("System prevented creating rack without name, height and depth") - create_child_frame.wait_for_timeout(2000) - - # 3. Тест: Заполняем только поле 'Высота в юнитах' - logger.info("Test 3: Only 'Height in units' field is filled") - - # Очистить поля - create_child_frame.clear_combobox_field("Глубина (мм)") - create_child_frame.clear_combobox_field("Высота в юнитах") - - # Очистить поля через заполнение пустыми значениями - rack_maker.fill_rack_data( - name="", - height="42", - depth="" - ) - - # Нажимаем кнопку создания без заполнения данных - create_child_frame.click_add_button() - create_child_frame.wait_for_timeout(2000) - - # Проверяем подсветку полей 'Имя' и 'Глубина (мм)' цветом ошибки - create_child_frame.check_field_not_highlighted_error("Высота в юнитах") - create_child_frame.check_field_highlighted_error("Глубина (мм)") - - # Проверяем, что НЕТ alert-окна для поля 'Высота в юнитах' - create_child_frame.alert.check_alert_absence(expected_alert_text_height, 1000) - - # Проверяем наличие alert-окна с сообщением о заполнении Глубины (мм) - create_child_frame.alert.check_alert_presence(expected_alert_text_depth) - # Закрываем alert-окно Глубины (мм) - create_child_frame.alert.close_alert_by_text(expected_alert_text_depth) - - # Проверяем, что остались на той же странице - create_child_frame.check_toolbar_title('Создать дочерний элемент в') - logger.info("System prevented creating rack without name and depth") - create_child_frame.wait_for_timeout(2000) - - # 4. Тест: Заполняем только поле 'Глубина (мм)' - logger.info("Test 4: Only 'Depth (mm)' field is filled") - - create_child_frame.clear_combobox_field("Глубина (мм)") - create_child_frame.clear_combobox_field("Высота в юнитах") - - rack_maker.fill_rack_data( - name="", - height="", - depth="1000" - ) - - create_child_frame.wait_for_timeout(5000) - - # Нажимаем кнопку создания - create_child_frame.click_add_button() - create_child_frame.wait_for_timeout(3000) - - # Проверяем подсветку полей 'Имя' и 'Высота в юнитах' цветом ошибки - create_child_frame.check_field_highlighted_error("Высота в юнитах") - create_child_frame.check_field_not_highlighted_error("Глубина (мм)") - - logger.info("Validation for 'Depth' field alert absence temporarily disabled") - - # Проверяем наличие alert-окна с сообщением о заполнении Высота в юнитах - create_child_frame.alert.check_alert_presence(expected_alert_text_height) - # Закрываем alert-окно Высота в юнитах - create_child_frame.alert.close_alert_by_text(expected_alert_text_height) - - # Проверяем, что остались на той же странице - create_child_frame.check_toolbar_title('Создать дочерний элемент в') - logger.info("System prevented creating rack without name and height") - create_child_frame.wait_for_timeout(2000) + # Выполняем тестовые случаи + for test_case in test_cases: + logger.info(test_case["name"]) + self._perform_required_fields_test( + create_child_frame, rack_maker, test_case["data"] + ) + logger.info("System prevented creating rack with invalid required fields") # 5. Тест: Заполняем все обязательные поля logger.info("Test 5: All required fields are filled") @@ -364,7 +347,6 @@ class TestCreateRackElement: create_child_frame.wait_for_timeout(3000) # Проверяем, что НЕТ alert-окон для всех обязательных полей - create_child_frame.alert.check_alert_absence(expected_alert_text_name, 1000) create_child_frame.alert.check_alert_absence(expected_alert_text_height, 1000) create_child_frame.alert.check_alert_absence(expected_alert_text_depth, 1000) logger.info("No alert windows for required fields appeared - all fields filled correctly") @@ -373,7 +355,7 @@ class TestCreateRackElement: try: create_child_frame.check_toolbar_title('Создать дочерний элемент в') logger.warning("Rack creation may not have completed successfully") - except Exception as e: + except AssertionError: logger.info("Creation page closed - rack successfully created") logger.info("Required fields validation test completed successfully") @@ -397,11 +379,11 @@ class TestCreateRackElement: if element.is_visible(): logger.info(f"Rack with name '{rack_name}' found") return True - else: - logger.info(f"Rack with name '{rack_name}' not found") - return False - def _create_rack(self, browser: Page, rack_name: str) -> CreateChildElementFrame: + logger.info(f"Rack with name '{rack_name}' not found") + return False + + def _create_rack(self, browser: Page, rack_name: str) -> None: """Создает стойку.""" logger.info(f"Creating rack with name '{rack_name}'") @@ -410,7 +392,10 @@ class TestCreateRackElement: self.main_page.wait_for_timeout(2000) # Нажимаем кнопку "Создать" на тулбаре - create_child_frame = self.location_page.click_create_button() + self.location_page.click_create_button() + + # Создаем фрейм создания дочернего элемента + create_child_frame = CreateChildElementFrame(browser) # Нажимаем на плашку "Класс объекта учета" create_child_frame.open_object_class_combobox() @@ -431,5 +416,3 @@ class TestCreateRackElement: # Нажимаем кнопку создания create_child_frame.click_add_button() create_child_frame.wait_for_timeout(2000) - - return create_child_frame diff --git a/tests/e2e/test_expand_navigation_panel.py b/tests/e2e/test_expand_navigation_panel.py index 0689d55..633b6f7 100644 --- a/tests/e2e/test_expand_navigation_panel.py +++ b/tests/e2e/test_expand_navigation_panel.py @@ -100,7 +100,7 @@ class TestNavigationPanel: mp.wait_for_timeout(5000) - mp.click_subpanel_item("Физические устройства с опросом") + mp.click_subpanel_item("test-zone") mp.wait_for_timeout(3000) # Переходим Здание ЦОД 4 diff --git a/tools/__pycache__/__init__.cpython-313.pyc b/tools/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 71c14bdef49081c6e468953f991f7329772a8cfe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 312 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