From b1fffac8122ebd53971defa9fa8437cd51cd9188 Mon Sep 17 00:00:00 2001 From: Radislav Date: Wed, 10 Dec 2025 11:30:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D0=BA=D0=BE=D0=B4=D0=B0:=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D0=B4=D0=B0=D1=80=D1=82=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20assert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены пустые строки после docstrings - Заменены raise AssertionError на assert --- .../accounting_objects/rack_maker.py | 7 + .../frames/create_child_element_frame.py | 247 +++++++++--------- components_derived/selection_bar_component.py | 26 +- .../test_create_rack_element.py | 86 ++++-- 4 files changed, 217 insertions(+), 149 deletions(-) diff --git a/components_derived/accounting_objects/rack_maker.py b/components_derived/accounting_objects/rack_maker.py index d2de9a3..f684208 100644 --- a/components_derived/accounting_objects/rack_maker.py +++ b/components_derived/accounting_objects/rack_maker.py @@ -12,6 +12,7 @@ logger = get_logger("RACK_MAKER") @dataclass class RackData: """Класс для хранения данных стойки.""" + name: str height: str = "42" depth: str = "1000" @@ -35,6 +36,7 @@ class RackObjectMaker(BaseComponent): Args: page: Экземпляр страницы Playwright """ + super().__init__(page) # Действия: @@ -46,6 +48,7 @@ class RackObjectMaker(BaseComponent): Args: rack_data: Данные стойки """ + logger.info(f"Filling rack data: {rack_data.name}") self._fill_text_fields(rack_data) @@ -58,6 +61,7 @@ class RackObjectMaker(BaseComponent): def clear_and_fill(locator, value: str, field_name: str): """Очищает поле и заполняет его значением.""" + field = self.page.locator(locator).first # Очищаем поле field.click() @@ -130,6 +134,7 @@ class RackObjectMaker(BaseComponent): value: Значение для установки field_locator: Локатор поля """ + logger.info(f"Filling field '{field_name}' with value '{value}'...") # Используем first() для избежания strict mode violation @@ -163,6 +168,7 @@ class RackObjectMaker(BaseComponent): Returns: str: Локатор поля """ + field_map = { "Имя": RackLocators.RACK_NAME_FIELD, "Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD, @@ -183,6 +189,7 @@ class RackObjectMaker(BaseComponent): Raises: AssertionError: Если какое-либо поле не найдено """ + logger.info("Checking rack fields presence...") # Основные обязательные поля diff --git a/components_derived/frames/create_child_element_frame.py b/components_derived/frames/create_child_element_frame.py index 908f5d2..caabd8a 100644 --- a/components_derived/frames/create_child_element_frame.py +++ b/components_derived/frames/create_child_element_frame.py @@ -23,6 +23,7 @@ class CreateChildElementFrame(BaseComponent): Args: page: Экземпляр страницы Playwright """ + super().__init__(page) # Инициализация компонентов @@ -46,6 +47,34 @@ class CreateChildElementFrame(BaseComponent): # Действия: + def clear_combobox_field(self, field_name: str) -> None: + """ + Очищает combobox поле по его названию. + + Args: + field_name: Название поля для очистки + """ + + logger.info(f"Clearing combobox field '{field_name}'...") + + # Получаем локатор поля по его названию + field_locator = self._get_field_locator(field_name) + + # Используем метод из SelectionBarComponent + self.selection_bar.clear_combobox_field(field_name, field_locator) + + def click_add_button(self) -> None: + """Кликает на кнопку 'Добавить'.""" + + logger.info("Clicking on 'Add' button...") + self.toolbar.click_button("add") + + def click_cancel_button(self) -> None: + """Кликает на кнопку 'Отменить'.""" + + logger.info("Clicking on 'Cancel' button...") + self.toolbar.click_button("cancel") + def get_object_class_options(self) -> list[str]: """ Получает список доступных опций из combobox. @@ -53,6 +82,7 @@ class CreateChildElementFrame(BaseComponent): Returns: list[str]: Список доступных классов объектов """ + logger.info("Getting combobox 'Accounting object class' options...") available_options = self.selection_bar.get_available_options() @@ -70,62 +100,9 @@ class CreateChildElementFrame(BaseComponent): return self.selection_bar.get_selection_bar_title() - def _get_field_locator(self, field_name: str) -> str: - """ - Возвращает локатор поля по его названию. - - Args: - field_name: Название поля - - Returns: - str: Локатор поля - """ - field_map = { - "Имя": RackLocators.RACK_NAME_FIELD, - "Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD, - "Глубина (мм)": RackLocators.RACK_DEPTH_FIELD, - "Серийный номер": RackLocators.RACK_SERIAL_FIELD, - "Инвентарный номер": RackLocators.RACK_INVENTORY_FIELD, - "Комментарий": RackLocators.RACK_COMMENT_FIELD, - "Ввод кабеля": RackLocators.RACK_CABLE_ENTRY_FIELD, - "Состояние": RackLocators.RACK_STATE_FIELD, - "Владелец": RackLocators.RACK_OWNER_FIELD, - "Обслуживающая организация": RackLocators.RACK_SERVICE_ORG_FIELD, - "Проект/Титул": RackLocators.RACK_PROJECT_FIELD - } - - if field_name not in field_map: - raise ValueError(f"Locator for field '{field_name}' not found") - - return field_map[field_name] - - def clear_combobox_field(self, field_name: str) -> None: - """ - Очищает combobox поле по его названию. - - Args: - field_name: Название поля для очистки - """ - logger.info(f"Clearing combobox field '{field_name}'...") - - # Получаем локатор поля по его названию - field_locator = self._get_field_locator(field_name) - - # Используем метод из SelectionBarComponent - self.selection_bar.clear_combobox_field(field_name, field_locator) - - def click_add_button(self) -> None: - """Кликает на кнопку 'Добавить'.""" - logger.info("Clicking on 'Add' button...") - self.toolbar.click_button("add") - - def click_cancel_button(self) -> None: - """Кликает на кнопку 'Отменить'.""" - logger.info("Clicking on 'Cancel' button...") - self.toolbar.click_button("cancel") - def open_object_class_combobox(self) -> None: """Открывает выпадающий список combobox 'Класс объекта учета'.""" + logger.info("Opening combobox 'Accounting object class'...") # Ждем стабильности combobox @@ -148,6 +125,7 @@ class CreateChildElementFrame(BaseComponent): def select_object_class(self, class_name: str) -> None: """Выбирает класс объекта из выпадающего списка.""" + logger.info(f"Selecting object class: '{class_name}'...") # Открываем combobox @@ -170,73 +148,6 @@ class CreateChildElementFrame(BaseComponent): # Проверки: - def check_object_class_selected(self, expected_class: str) -> None: - """ - Проверяет что выбран указанный класс объекта. - - Args: - expected_class: Ожидаемый выбранный класс объекта - - Raises: - AssertionError: Если выбранный класс не соответствует ожидаемому - """ - logger.info(f"Checking selected object class: '{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"Object class '{expected_class}' successfully selected " - f"(actual: '{actual_class}')" - ) - else: - error_msg = ( - f"Selected class does not match expected. " - f"Expected: '{expected_class}', Got: '{actual_class}'" - ) - raise AssertionError(error_msg) - - def check_toolbar_title(self, expected_title: str) -> None: - """ - Проверяет заголовок тулбара. - - Args: - expected_title: Ожидаемый заголовок тулбара - - Raises: - AssertionError: Если заголовок не соответствует ожидаемому - """ - logger.info(f"Checking toolbar title: '{expected_title}'...") - - # Используем метод тулбара с фильтрацией по тексту - actual_text = self.toolbar.get_toolbar_title_text( - filter_text="Создать дочерний элемент в" - ) - assert expected_title in actual_text, ( - f"Title does not match. Expected: '{expected_title}', " - f"Got: '{actual_text}'" - ) - - logger.info(f"Toolbar title is correct: '{actual_text}'") - - 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) - - # Методы проверки ошибок полей (используют SelectionBarComponent) - def check_field_error_highlighted(self, field_name: str) -> None: """ Проверяет, что поле подсвечено цветом ошибки (валидация не пройдена). @@ -244,6 +155,7 @@ class CreateChildElementFrame(BaseComponent): Args: field_name: Название поля для проверки """ + field_locator = self._get_field_locator(field_name) self.selection_bar.check_field_error_highlighted(field_name, field_locator) @@ -254,5 +166,98 @@ class CreateChildElementFrame(BaseComponent): Args: field_name: Название поля для проверки """ + field_locator = self._get_field_locator(field_name) self.selection_bar.check_field_error_not_highlighted(field_name, field_locator) + + def check_object_class_selected(self, expected_class: str) -> None: + """ + Проверяет что выбран указанный класс объекта. + + Args: + expected_class: Ожидаемый выбранный класс объекта + """ + + logger.info(f"Checking selected object class: '{expected_class}'...") + + self.wait_for_timeout(1000) + actual_class = self.get_selected_object_class() + + is_match = (expected_class.lower() in actual_class.lower() or + actual_class.lower() in expected_class.lower()) + + assert is_match, ( + f"Selected class does not match expected. " + f"Expected: '{expected_class}', Got: '{actual_class}'" + ) + + logger.info( + f"Object class '{expected_class}' successfully selected " + f"(actual: '{actual_class}')" + ) + + def check_toolbar_title(self, expected_title: str) -> None: + """ + Проверяет заголовок тулбара. + + Args: + expected_title: Ожидаемый заголовок тулбара + """ + + logger.info(f"Checking toolbar title: '{expected_title}'...") + + # Используем метод тулбара с фильтрацией по тексту + actual_text = self.toolbar.get_toolbar_title_text( + filter_text="Создать дочерний элемент в" + ) + + assert expected_title in actual_text, ( + f"Title does not match. Expected: '{expected_title}', " + f"Got: '{actual_text}'" + ) + + logger.info(f"Toolbar title is correct: '{actual_text}'") + + def should_be_toolbar_buttons(self) -> None: + """ + Проверяет наличие и функциональность кнопок тулбара. + """ + + self.wait_for_timeout(2000) + + self.toolbar.check_button_visibility("add") + self.toolbar.check_button_tooltip("add", "Добавить") + self.toolbar.check_button_visibility("cancel") + self.toolbar.check_button_tooltip("cancel", "Отменить") + self.toolbar.click_button("cancel") + self.wait_for_timeout(2000) + + def _get_field_locator(self, field_name: str) -> str: + """ + Возвращает локатор поля по его названию. + + Args: + field_name: Название поля + + Returns: + str: Локатор поля + """ + + field_map = { + "Имя": RackLocators.RACK_NAME_FIELD, + "Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD, + "Глубина (мм)": RackLocators.RACK_DEPTH_FIELD, + "Серийный номер": RackLocators.RACK_SERIAL_FIELD, + "Инвентарный номер": RackLocators.RACK_INVENTORY_FIELD, + "Комментарий": RackLocators.RACK_COMMENT_FIELD, + "Ввод кабеля": RackLocators.RACK_CABLE_ENTRY_FIELD, + "Состояние": RackLocators.RACK_STATE_FIELD, + "Владелец": RackLocators.RACK_OWNER_FIELD, + "Обслуживающая организация": RackLocators.RACK_SERVICE_ORG_FIELD, + "Проект/Титул": RackLocators.RACK_PROJECT_FIELD + } + + if field_name not in field_map: + raise ValueError(f"Locator for field '{field_name}' not found") + + return field_map[field_name] diff --git a/components_derived/selection_bar_component.py b/components_derived/selection_bar_component.py index 5f8e661..471138b 100644 --- a/components_derived/selection_bar_component.py +++ b/components_derived/selection_bar_component.py @@ -27,6 +27,7 @@ class SelectionBarComponent(BaseComponent): locator_or_text: Локатор панели выбора значения (строка или объект Locator) или текст для поиска """ + super().__init__(page) # Определяем локатор в зависимости от типа параметра @@ -47,6 +48,7 @@ class SelectionBarComponent(BaseComponent): # Действия: def clear_selections(self) -> None: """Удаление ранее выбранных значений""" + selected_values = self.get_selected_values() if len(selected_values) > 0: clear_button_locator = self.selection_bar_locator.locator( @@ -60,6 +62,7 @@ class SelectionBarComponent(BaseComponent): Returns: list[str]: Список доступных опций """ + logger.info("Getting available options from dropdown list...") # Открываем выпадающий список @@ -82,11 +85,13 @@ class SelectionBarComponent(BaseComponent): def get_selection_bar_title(self) -> str: """Возвращает название панели выбора значения""" + title_locator = self.selection_bar_locator.locator(SelectionBarLocators.TITLE_LOCATOR) return title_locator.text_content() def get_selected_values(self) -> list[str]: """Возвращает список выбранных значений""" + selected_values_locator = self.selection_bar_locator.locator( SelectionBarLocators.PARAMETERS_SELECTED ) @@ -100,6 +105,7 @@ class SelectionBarComponent(BaseComponent): field_name: Название поля для очистки field_locator: Локатор поля combobox """ + logger.info(f"Clearing combobox field '{field_name}' using close button...") # Находим поле по локатору @@ -132,6 +138,7 @@ class SelectionBarComponent(BaseComponent): def open_values_list(self) -> None: """Открытие выпадающего списка путем нажатия на панель выбора значения""" + expect(self.selection_bar_locator).to_be_visible() # Проверяем, не открыт ли уже список @@ -149,17 +156,10 @@ class SelectionBarComponent(BaseComponent): def select_value(self, name: str) -> None: """Выбор значения из списка""" + self.selected_values_list.check_item_with_text(name) self.selected_values_list.click_item_with_text(name) - def wait_for_timeout(self, timeout: int) -> None: - """Ожидает указанное количество миллисекунд. - - Args: - timeout: Время ожидания в миллисекундах - """ - self.page.wait_for_timeout(timeout) - # Проверки: def check_field_error_highlighted(self, field_name: str, field_locator: str) -> None: @@ -169,6 +169,7 @@ class SelectionBarComponent(BaseComponent): field_name: Название поля для проверки field_locator: Локатор поля для проверки """ + logger.info(f"Checking field '{field_name}' for error highlighting...") field_element = self.page.locator(field_locator).first @@ -183,8 +184,7 @@ class SelectionBarComponent(BaseComponent): if parent_container.count() > 0: has_error = parent_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS).count() > 0 - if not has_error: - raise AssertionError(f"Field '{field_name}' is not highlighted with error color") + assert has_error, f"Field '{field_name}' is not highlighted with error color" logger.info(f"Field '{field_name}' is correctly highlighted with error color") @@ -195,6 +195,7 @@ class SelectionBarComponent(BaseComponent): field_name: Название поля для проверки field_locator: Локатор поля для проверки """ + logger.info(f"Checking field '{field_name}' for absence of error highlighting...") field_element = self.page.locator(field_locator).first @@ -205,11 +206,10 @@ class SelectionBarComponent(BaseComponent): # Ищем родительский контейнер parent_container = field_element.locator(SelectionBarLocators.INPUT_PARENT_CONTAINER).first - # Проверяем отсутствие классов ошибки с использованием локатора из SelectionBarLocators + # Проверяем отсутствие классов ошибки if parent_container.count() > 0: has_error = parent_container.locator(SelectionBarLocators.ERROR_CSS_SELECTORS).count() > 0 - if has_error: - raise AssertionError(f"Field '{field_name}' is highlighted with error") + assert not has_error, f"Field '{field_name}' is highlighted with error" logger.info(f"Field '{field_name}' correctly has no error highlighting") diff --git a/tests/e2e/create_elements/test_create_rack_element.py b/tests/e2e/create_elements/test_create_rack_element.py index a72f70b..ad19db6 100644 --- a/tests/e2e/create_elements/test_create_rack_element.py +++ b/tests/e2e/create_elements/test_create_rack_element.py @@ -35,6 +35,7 @@ class TestCreateRackElement: Args: browser (Page): Экземпляр страницы Playwright для взаимодействия с UI """ + # Авторизация в системе login_page = LoginPage(browser) login_page.do_login() @@ -103,10 +104,6 @@ class TestCreateRackElement: # Открывается набор плашек для задания параметров стойки rack_maker = RackObjectMaker(browser) - # Проверяем что после выбора 'Стойка' появляются специфичные поля - rack_maker.check_rack_fields_presence() - logger.info("Rack-specific fields are displayed correctly") - # Создаем объект данных стойки rack_data = RackData( name="Test-Rack-01", @@ -135,12 +132,13 @@ class TestCreateRackElement: Проверяет, что система корректно обрабатывает попытку создания стойки с именем, которое уже используется. """ + logger.info("Starting test for creating rack with duplicate name") rack_name = "Test-Rack-01" # Проверяем, существует ли уже стойка с таким именем - if not self._check_rack_exists(browser, rack_name): + if not self._check_rack_existance(browser, rack_name): logger.info(f"Rack with name '{rack_name}' not found. Creating first rack.") self._create_rack(browser, rack_name) logger.info(f"First rack with name '{rack_name}' created successfully") @@ -204,6 +202,7 @@ class TestCreateRackElement: rack_maker: Объект для работы со стойкой test_data: Словарь с данными теста """ + # Распаковываем данные теста name_value = test_data["name"] height_value = test_data["height"] @@ -211,9 +210,53 @@ class TestCreateRackElement: 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("Высота в юнитах") + # Функция для проверки заполненности combobox поля + def is_field_filled(field_name: str) -> bool: + """Проверяет, заполнено ли combobox поле.""" + + # Получаем локатор поля + field_locator = create_child_frame._get_field_locator(field_name) + + # Находим элемент поля + field_element = create_child_frame.page.locator(field_locator).first + + if not field_element.is_visible(): + logger.info(f"Field '{field_name}' not visible") + return False + + # Проверяем наличие кнопки закрытия (крестика) - признак заполненного поля + close_button = field_element.locator( + ".v-select__selections" # Или другой локатор для кнопки закрытия + ) + + # Если есть кнопка закрытия, поле заполнено + has_close_button = close_button.count() > 0 and close_button.is_visible() + + # Также можно проверить по тексту в поле + field_text = field_element.text_content() or "" + has_text = bool(field_text.strip()) + + logger.info(f"Field '{field_name}' - has close button: {has_close_button}, has text: {has_text}") + + return has_close_button or has_text + + # Проверяем и очищаем поле "Глубина (мм)" только если оно заполнено + logger.info("Checking field: Depth (mm)") + if is_field_filled("Глубина (мм)"): + logger.info("Field 'Depth (mm)' is filled, performing clearing") + create_child_frame.clear_combobox_field("Глубина (мм)") + logger.info("Clearing completed for 'Depth (mm)'") + else: + logger.info("Field 'Depth (mm)' is already empty, skipping clearing") + + # Проверяем и очищаем поле "Высота в юнитах" только если оно заполнено + logger.info("Checking field: Height in units") + if is_field_filled("Высота в юнитах"): + logger.info("Field 'Height in units' is filled, performing clearing") + create_child_frame.clear_combobox_field("Высота в юнитах") + logger.info("Clearing completed for 'Height in units'") + else: + logger.info("Field 'Height in units' is already empty, skipping clearing") # Создаем объект данных стойки rack_data = RackData( @@ -222,35 +265,48 @@ class TestCreateRackElement: depth=depth_value ) - # Заполняем данные + # Заполняем данные стойки + logger.info(f"Setting test data - Name: '{name_value}', Height: '{height_value}', Depth: '{depth_value}'") rack_maker.fill_rack_data(rack_data) # Нажимаем кнопку создания + logger.info("Submitting form for validation") create_child_frame.click_add_button() create_child_frame.wait_for_timeout(3000) - # Проверяем подсветку полей + # Проверяем валидацию полей + logger.info("Checking validation results") + if height_value: create_child_frame.check_field_error_not_highlighted("Высота в юнитах") + logger.info("Height field validation passed") else: create_child_frame.check_field_error_highlighted("Высота в юнитах") + logger.info("Height field validation failed as expected") if depth_value: create_child_frame.check_field_error_not_highlighted("Глубина (мм)") + logger.info("Depth field validation passed") else: create_child_frame.check_field_error_highlighted("Глубина (мм)") + logger.info("Depth field validation failed as expected") # Обрабатываем alert-окна if not height_value: + logger.info("Expecting height validation alert") create_child_frame.alert.check_alert_presence(expected_alert_height) create_child_frame.alert.close_alert_by_text(expected_alert_height) + logger.info("Height alert handled") if not depth_value: + logger.info("Expecting depth validation alert") create_child_frame.alert.check_alert_presence(expected_alert_depth) create_child_frame.alert.close_alert_by_text(expected_alert_depth) + logger.info("Depth alert handled") - # Проверяем, что остались на той же странице + # Проверяем, что остались на странице создания create_child_frame.check_toolbar_title('Создать дочерний элемент в') + logger.info("Test completed successfully") def test_required_fields_validation(self, browser: Page) -> None: """ @@ -260,6 +316,7 @@ class TestCreateRackElement: - Поле 'Высота в юнитах' должно быть заполнено - Поле 'Глубина (мм)' должно быть заполнено """ + # Текст сообщения alert-окна expected_alert_text_height = "поле Высота в юнитах должно быть заполнено" expected_alert_text_depth = "поле Глубина (мм) должно быть заполнено" @@ -279,9 +336,6 @@ class TestCreateRackElement: # Открывается набор плашек для задания параметров стойки rack_maker = RackObjectMaker(browser) - # Проверяем наличие полей стойки - rack_maker.check_rack_fields_presence() - # Тестовые данные test_cases = [ { @@ -374,8 +428,9 @@ class TestCreateRackElement: logger.info("Required fields validation test completed successfully") - def _check_rack_exists(self, browser: Page, rack_name: str) -> bool: + def _check_rack_existance(self, browser: Page, rack_name: str) -> bool: """Проверяет существование стойки.""" + logger.info(f"Checking existence of rack with name '{rack_name}'") # Обновляем навигационную панель @@ -399,6 +454,7 @@ class TestCreateRackElement: def _create_rack(self, browser: Page, rack_name: str) -> None: """Создает стойку.""" + logger.info(f"Creating rack with name '{rack_name}'") # Переходим обратно к созданию новой стойки