From c0459e09057f3db18141d2d8a91ae67a9166db38 Mon Sep 17 00:00:00 2001 From: Radislav Date: Mon, 17 Nov 2025 15:21:59 +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=BF=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=BE=D0=B2=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20combobox=20?= =?UTF-8?q?=D0=B2=20DropdownList?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/dropdown_list_component.py | 109 ++- locators/alert_locators.py | 6 +- locators/rack_locators.py | 20 +- .../create_rack_element_tab.cpython-313.pyc | Bin 34051 -> 34936 bytes .../create_rack_element_tab.py | 805 +++++++++--------- ...child_element.cpython-313-pytest-8.4.1.pyc | Bin 9111 -> 0 bytes ..._rack_element.cpython-313-pytest-8.4.1.pyc | Bin 14228 -> 0 bytes .../test_create_rack_element.py | 126 +-- ...t_create_rack.cpython-313-pytest-8.4.1.pyc | Bin 5769 -> 0 bytes ..._general_info.cpython-313-pytest-8.4.1.pyc | Bin 3030 -> 0 bytes ...eneral_info__.cpython-313-pytest-8.4.1.pyc | Bin 2828 -> 0 bytes ...test_rack_tab.cpython-313-pytest-8.4.1.pyc | Bin 5904 -> 0 bytes ..._sessions_tab.cpython-313-pytest-8.4.1.pyc | Bin 26177 -> 0 bytes ..._settings_tab.cpython-313-pytest-8.4.1.pyc | Bin 9950 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 322 -> 0 bytes ...test_add_user.cpython-313-pytest-8.4.1.pyc | Bin 9207 -> 0 bytes ...est_edit_user.cpython-313-pytest-8.4.1.pyc | Bin 11345 -> 0 bytes ...est_user_card.cpython-313-pytest-8.4.1.pyc | Bin 12843 -> 0 bytes ...est_users_tab.cpython-313-pytest-8.4.1.pyc | Bin 3346 -> 0 bytes 19 files changed, 614 insertions(+), 452 deletions(-) delete mode 100644 tests/e2e/create_elements/__pycache__/test_create_child_element.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/create_elements/__pycache__/test_create_rack_element.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/rack/__pycache__/test_create_rack.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/rack/__pycache__/test_rack_general_info.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/rack/__pycache__/test_rack_general_info__.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/rack/__pycache__/test_rack_tab.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/sessions/__pycache__/test_current_sessions_tab.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/sessions/__pycache__/test_session_settings_tab.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/users/__pycache__/__init__.cpython-313.pyc delete mode 100644 tests/e2e/users/__pycache__/test_add_user.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/users/__pycache__/test_edit_user.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/users/__pycache__/test_user_card.cpython-313-pytest-8.4.1.pyc delete mode 100644 tests/e2e/users/__pycache__/test_users_tab.cpython-313-pytest-8.4.1.pyc diff --git a/components/dropdown_list_component.py b/components/dropdown_list_component.py index 160aa47..a139953 100644 --- a/components/dropdown_list_component.py +++ b/components/dropdown_list_component.py @@ -88,6 +88,113 @@ class DropdownList(BaseComponent): self.page.wait_for_timeout(300) self.check_item_with_text(last_item_name) + def open_combobox(self, combobox_locator: str | Locator, listbox_locator: str | Locator, icon_locator: str | Locator = None) -> None: + """ + Открывает выпадающий список combobox. + + Args: + combobox_locator: Локатор combobox + listbox_locator: Локатор выпадающего списка + icon_locator: Локатор иконки для клика (опционально) + """ + logger.info("Открытие combobox...") + + combobox = self.get_locator(combobox_locator) + listbox = self.get_locator(listbox_locator) + + # Прокручиваем до combobox + combobox.scroll_into_view_if_needed() + self.page.wait_for_timeout(1000) + + # Проверяем, не открыт ли уже список + listbox_already_open = False + listbox_count = listbox.count() + + if listbox_count > 0: + listbox_already_open = listbox.first.is_visible() + + if not listbox_already_open: + # Если указан локатор иконки, кликаем на него, иначе на сам combobox + if icon_locator: + icon = combobox.locator(icon_locator) + icon.scroll_into_view_if_needed() + icon.click(timeout=10000) + else: + combobox.click(timeout=10000) + logger.info("Клик на combobox выполнен") + self.page.wait_for_timeout(1000) + + # Проверяем что список открылся + listbox_count_after = listbox.count() + listbox_visible = False + + if listbox_count_after > 0: + listbox_visible = listbox.first.is_visible() + + if listbox_visible: + logger.info("Выпадающий список найден и открыт") + else: + logger.warning("Не удалось открыть выпадающий список") + + def get_combobox_options(self, combobox_locator: str | Locator, listbox_locator: str | Locator, icon_locator: str | Locator = None) -> list[str]: + """ + Получает список доступных опций из combobox. + + Args: + combobox_locator: Локатор combobox + listbox_locator: Локатор выпадающего списка + icon_locator: Локатор иконки для клика (опционально) + + Returns: + list[str]: Список доступных опций + """ + logger.info("Получение списка опций combobox...") + + # Открываем combobox (если еще не открыт) + self.open_combobox(combobox_locator, listbox_locator, icon_locator) + + options_list = self.get_item_names(listbox_locator) + + # Закрываем combobox (кликаем вне его) + self.page.mouse.click(10, 10) + self.page.wait_for_timeout(500) + + logger.info(f"Найдено опций: {len(options_list)} - {options_list}") + return options_list + + def get_selected_combobox_value(self, combobox_locator: str | Locator, value_locator: str | Locator = None) -> str: + """ + Получает выбранное значение из combobox. + + Args: + combobox_locator: Локатор combobox + value_locator: Локатор элемента с выбранным значением (опционально) + + Returns: + str: Выбранное значение или пустая строка если ничего не выбрано + """ + combobox = self.get_locator(combobox_locator) + + selected_value = "" + + if value_locator: + # Используем переданный локатор для значения + value_element = combobox.locator(value_locator) + if value_element.count() > 0: + selected_value = value_element.first.text_content().strip() + else: + # Ищем в span элементах по умолчанию + span_locator = combobox.locator("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"Выбранное значение combobox: '{selected_value}'") + return selected_value + # Проверки: def check_item_with_text(self, text: str) -> None: """Проверяет наличие и доступность элемента списка. @@ -98,7 +205,7 @@ class DropdownList(BaseComponent): Raises: AssertionError: Если элемент отсутствует или недоступен. """ - + element = self.page.get_by_role("listitem").filter(has_text=text) if element.count() > 1: rtext = f"^{text}$" diff --git a/locators/alert_locators.py b/locators/alert_locators.py index c21b587..12b1d86 100644 --- a/locators/alert_locators.py +++ b/locators/alert_locators.py @@ -14,10 +14,14 @@ class AlertLocators: ALERT_MESSAGE (str): текстового сообщения в alert-окне. ALERT_DISMISS_BUTTON (str): кнопки закрытия alert-окна. ALERT_BY_TEXT (str): alert-окна с определенным текстом (шаблон). + ERROR_CLASSES (list): классы для подсветки ошибок валидации. """ ALERT_ROLE: str = "alert" ALERT_BASE: str = "//div[contains(@class,'v-alert')]" ALERT_MESSAGE: str = f"{ALERT_BASE}/div" ALERT_DISMISS_BUTTON: str = "//a[@class='v-alert__dismissible']" - ALERT_BY_TEXT: str = f"{ALERT_BASE}[contains(., '{{text}}')]" \ No newline at end of file + ALERT_BY_TEXT: str = f"{ALERT_BASE}[contains(., '{{text}}')]" + + # Классы для подсветки ошибок валидации полей + ERROR_CLASSES: list = ["error--text"] diff --git a/locators/rack_locators.py b/locators/rack_locators.py index 17d4ccd..c79b5cd 100644 --- a/locators/rack_locators.py +++ b/locators/rack_locators.py @@ -11,7 +11,7 @@ class RackLocators: - Вкладки стойки (верхние вкладки) - Секции лицевой и обратной сторон стойки - Юниты и устройства на стойке - - Поля формы редактирования стойки + - Поля формы редактирования и создания стойки - Контейнеры и структурные элементы """ @@ -39,7 +39,7 @@ class RackLocators: # Контейнер формы FORM_CONTAINER = "//div[contains(@class, 'container')]" - # Локаторы полей + # Локаторы полей формы редактирования стойки NAME_FIELD = "//input[@aria-label='Имя']" SERIAL_NUMBER_FIELD = "//input[@aria-label='Серийный номер']" INVENTORY_NUMBER_FIELD = "//input[@aria-label='Инвентарный номер']" @@ -50,6 +50,22 @@ class RackLocators: SERVICE_ORG_FIELD = "//input[@aria-label='Обслуживающая организация']" PROJECT_FIELD = "//input[@aria-label='Проект/Титул']" + # Локаторы полей формы создания стойки + RACK_NAME_FIELD = "//label[text()='Имя']/following-sibling::input" + RACK_HEIGHT_FIELD = "//div[contains(@class, 'v-input__slot') and .//label[text()='Высота в юнитах']]" + RACK_DEPTH_FIELD = "//div[contains(@class, 'v-input__slot') and .//label[text()='Глубина (мм)']]" + RACK_SERIAL_FIELD = "//label[text()='Серийный номер']/following-sibling::input" + RACK_INVENTORY_FIELD = "//label[text()='Инвентарный номер']/following-sibling::input" + RACK_COMMENT_FIELD = "//label[text()='Комментарий']/following-sibling::input" + RACK_CABLE_ENTRY_FIELD = "//div[contains(@class, 'v-input__slot') and .//label[text()='Ввод кабеля']]" + RACK_STATE_FIELD = "//div[contains(@class, 'container')]//div[contains(@class, 'v-input__slot white') and .//label[text()='Состояние']]" + RACK_OWNER_FIELD = "//div[contains(@class, 'v-input__slot') and .//label[text()='Владелец']]" + RACK_SERVICE_ORG_FIELD = "//div[contains(@class, 'v-input__slot') and .//label[text()='Обслуживающая организация']]" + RACK_PROJECT_FIELD = "//div[contains(@class, 'v-input__slot') and .//label[text()='Проект/Титул']]" + + # Локатор для родительского контейнера поля ввода + INPUT_PARENT_CONTAINER = "xpath=./ancestor::div[contains(@class, 'v-input')]" + # Локаторы для отображения сторон стойки FRONT_SIDE_CONTAINER = "//div[contains(@class, 'cabinet') and not(contains(@class, 'back'))]" BACK_SIDE_CONTAINER = "//div[contains(@class, 'cabinet') and contains(@class, 'back')]" diff --git a/pages/create_elements_tab/__pycache__/create_rack_element_tab.cpython-313.pyc b/pages/create_elements_tab/__pycache__/create_rack_element_tab.cpython-313.pyc index 1178e22652c10ffbe7691266570a1d740ebaf562..a8b984260b98b698823e061c3f52a51492112924 100644 GIT binary patch delta 8216 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 bm-EuHvLymzoGCe|V)wGAYOXRgL+A5f*L!-6 delta 7842 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 None: + """ + Кликает на кнопку 'Добавить'. + """ + self.toolbar.click_button("add") + + def click_cancel_button(self) -> None: + """ + Кликает на кнопку 'Отменить'. + """ + self.toolbar.click_button("cancel") + + def open_object_class_combobox(self) -> None: + """ + Открывает выпадающий список combobox 'Класс объекта учета'. + """ + logger.info("Открытие combobox 'Класс объекта учета'...") + self.dropdown.open_combobox( + ComboboxLocators.OBJECT_CLASS_COMBOBOX, + ComboboxLocators.LISTBOX_SELECTOR, + ComboboxLocators.COMBOBOX_ICON + ) + + 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 fill_rack_data(self, name: str, height: str = "42", depth: str = "1000", + serial: str = "", inventory: str = "", comment: str = "", + cable_entry: str = "", state: str = "", owner: str = "", + service_org: str = "", project: str = "") -> None: + """ + Заполняет данные для создания стойки. + + Args: + name: Наименование стойки + height: Высота в юнитах (по умолчанию 42) + depth: Глубина в мм (по умолчанию 1000) + serial: Серийный номер + inventory: Инвентарный номер + comment: Комментарий + cable_entry: Ввод кабеля + state: Состояние + owner: Владелец + service_org: Обслуживающая организация + project: Проект/Титул + """ + logger.info(f"Заполнение данных стойки: {name}") + + # Заполняем обязательные поля + name_field = self.page.locator(RackLocators.RACK_NAME_FIELD) + name_field.fill(name) + logger.info(f"Заполнено поле 'Имя': {name}") + + self._select_combobox("Высота в юнитах", height) + logger.info(f"Выбрана высота: {height} юнитов") + + self._select_combobox("Глубина (мм)", depth) + logger.info(f"Выбрана глубина: {depth} мм") + + # Заполняем опциональные поля + if serial: + serial_field = self.page.locator(RackLocators.RACK_SERIAL_FIELD) + serial_field.fill(serial) + logger.info(f"Заполнен серийный номер: {serial}") + + if inventory: + inventory_field = self.page.locator(RackLocators.RACK_INVENTORY_FIELD) + inventory_field.fill(inventory) + logger.info(f"Заполнен инвентарный номер: {inventory}") + + if comment: + comment_field = self.page.locator(RackLocators.RACK_COMMENT_FIELD) + comment_field.fill(comment) + logger.info(f"Добавлен комментарий: {comment}") + + # Заполняем дополнительные combobox поля + if cable_entry: + self._select_combobox("Ввод кабеля", cable_entry) + logger.info(f"Выбран ввод кабеля: {cable_entry}") + + if state: + self._select_combobox("Состояние", state) + logger.info(f"Выбрано состояние: {state}") + + if owner: + self._select_combobox("Владелец", owner) + logger.info(f"Выбран владелец: {owner}") + + if service_org: + self._select_combobox("Обслуживающая организация", service_org) + logger.info(f"Выбрана обслуживающая организация: {service_org}") + + if project: + self._select_combobox("Проект/Титул", project) + logger.info(f"Выбран проект/титул: {project}") + + logger.info("Данные стойки заполнены") + + def _select_combobox(self, field_name: str, value: str) -> None: + """ + Выбор значения в combobox. + + Args: + field_name: Название поля + value: Значение для выбора + """ + logger.info(f"Выбор '{value}' в поле '{field_name}'...") + + # Получаем статический локатор из словаря + if field_name not in COMBOBOX_FIELDS_MAP: + raise ValueError(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP") + + field_locator = COMBOBOX_FIELDS_MAP[field_name] + + # Для всех полей используем first() чтобы избежать strict mode violation + field_container = self.page.locator(field_locator).first + + # Прокручиваем до поля + field_container.scroll_into_view_if_needed() + self.wait_for_timeout(500) + + # Проверяем видимость поля + self.base_component.check_visibility(field_container, f"Поле '{field_name}' не найдено") + + # Универсальный клик с force=True для всех полей + field_container.click(force=True) + self.wait_for_timeout(1000) + + # Вводим значение + self.page.keyboard.type(value) + self.wait_for_timeout(500) + self.page.keyboard.press("Enter") + + logger.info(f"Поле '{field_name}' заполнено") + + def create_rack(self, rack_name: str, **kwargs) -> None: + """ + Полный процесс создания стойки. + + Args: + rack_name: Наименование стойки + **kwargs: Дополнительные параметры стойки + """ + logger.info(f"Начало процесса создания стойки: {rack_name}") + + # Выбираем класс объекта "Стойка" + self.select_object_class("Стойка") + self.wait_for_timeout(1000) + + # Проверяем наличие полей стойки + self.check_rack_fields_presence() + + # Заполняем данные + self.fill_rack_data(rack_name, **kwargs) + + # Создаем стойку + self.click_add_button() + + logger.info(f"Процесс создания стойки '{rack_name}' завершен") + + def clear_combobox_field(self, field_name: str) -> None: + """ + Очищает значение в combobox поле с помощью кнопки закрытия (крестика). + + Args: + field_name: Название поля для очистки + """ + logger.info(f"Очистка combobox поля '{field_name}' с помощью кнопки закрытия...") + + if field_name not in COMBOBOX_FIELDS_MAP: + logger.warning(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP") + return + + field_locator = COMBOBOX_FIELDS_MAP[field_name] + + # Находим поле по локатору + field_container = self.page.locator(field_locator).first + + # Проверяем что поле видимо + if not field_container.is_visible(): + logger.info(f"Поле '{field_name}' не видимо, пропускаем очистку") + return + + # Прокручиваем до поля + field_container.scroll_into_view_if_needed() + self.wait_for_timeout(500) + + # Ищем кнопку закрытия (крестик) внутри контейнера поля + close_button = field_container.locator(ComboboxLocators.COMBOBOX_CLOSE_BUTTON) + + # Проверяем наличие и видимость кнопки закрытия + if close_button.count() > 0 and close_button.is_visible(): + # Если кнопка закрытия видима - кликаем на нее + close_button.click() + self.wait_for_timeout(500) + logger.info(f"Combobox поле '{field_name}' очищено с помощью кнопки закрытия") + else: + # Если кнопки закрытия нет, просто логируем этот факт + logger.info(f"Кнопка закрытия не найдена для поля '{field_name}', очистка не выполнена") + + def clear_rack_fields(self) -> None: + """ + Очищает все поля формы создания стойки. + """ + logger.info("Очистка всех полей формы стойки...") + + # Очищаем текстовые поля + text_fields = [ + (RackLocators.RACK_NAME_FIELD, "Имя"), + (RackLocators.RACK_SERIAL_FIELD, "Серийный номер"), + (RackLocators.RACK_INVENTORY_FIELD, "Инвентарный номер"), + (RackLocators.RACK_COMMENT_FIELD, "Комментарий") + ] + + for field_locator, field_name in text_fields: + field = self.page.locator(field_locator) + if field.count() > 0 and field.first.is_visible(): + field.fill("") + logger.info(f"Текстовое поле '{field_name}' очищено") + + # Очищаем combobox поля + combobox_fields = [ + "Высота в юнитах", + "Глубина (мм)", + "Ввод кабеля", + "Состояние", + "Владелец", + "Обслуживающая организация", + "Проект/Титул" + ] + + for field_name in combobox_fields: + self.clear_combobox_field(field_name) + + logger.info("Все поля формы стойки очищены") + + def get_object_class_options(self) -> list[str]: + """ + Получает список доступных опций из combobox. + + Returns: + list[str]: Список доступных классов объектов + """ + return self.dropdown.get_combobox_options( + ComboboxLocators.OBJECT_CLASS_COMBOBOX, + ComboboxLocators.LISTBOX_SELECTOR, + ComboboxLocators.COMBOBOX_ICON + ) + + def get_selected_object_class(self) -> str: + """ + Получает выбранный класс объекта учета. + + Returns: + str: Выбранный класс объекта или пустая строка если ничего не выбрано + """ + return self.dropdown.get_selected_combobox_value( + ComboboxLocators.OBJECT_CLASS_COMBOBOX, + ComboboxLocators.SELECTED_VALUE_SPAN + ) + + # =============== МЕТОДЫ ПРОВЕРОК ======================== + def check_rack_exists(self, rack_name: str) -> bool: + """ + Проверяет, существует ли уже стойка с указанным именем в навигационной панели. + + Args: + rack_name: Имя стойки для проверки + + Returns: + bool: True если стойка существует, False если нет + """ + logger.info(f"Проверка существования стойки с именем '{rack_name}'") + + self.main_page.click_main_navigation_panel_item("Объекты") + self.main_page.click_main_navigation_panel_item("Объекты") + self.wait_for_timeout(1000) + self.main_page.click_subpanel_item("test-zone") + self.wait_for_timeout(3000) + + nav_panel_locator = NavigationPanelLocators.TREEVIEW + + # Проверяем видимость элемента через is_visible + element = self.page.locator(nav_panel_locator).get_by_text(rack_name).first + + if element.is_visible(): + logger.info(f"Стойка с именем '{rack_name}' найдена") + return True + else: + logger.info(f"Стойки с именем '{rack_name}' не найдена") + return False + def should_be_toolbar_buttons(self) -> None: """ Проверяет наличие и функциональность кнопок тулбара. @@ -108,18 +423,6 @@ class CreateRackElementTab(BasePage): self.toolbar.click_button("cancel") self.wait_for_timeout(2000) - def click_add_button(self) -> None: - """ - Кликает на кнопку 'Добавить'. - """ - self.toolbar.click_button("add") - - def click_cancel_button(self) -> None: - """ - Кликает на кнопку 'Отменить'. - """ - self.toolbar.click_button("cancel") - def check_toolbar_title(self, expected_title: str) -> None: """ Проверяет заголовок тулбара. @@ -130,7 +433,7 @@ class CreateRackElementTab(BasePage): Raises: AssertionError: Если заголовок не соответствует ожидаемому """ - logger.info(f"Проверка заголовка тулбара: '{expected_title}'...") + logger.info(f"Проверка заголовок тулбара: '{expected_title}'...") # Используем метод тулбара с фильтрацией по тексту actual_text = self.toolbar.get_toolbar_title_text( @@ -184,120 +487,6 @@ class CreateRackElementTab(BasePage): logger.info("Содержимое combobox 'Класс объекта учета' корректно") - def open_object_class_combobox(self) -> None: - """ - Открывает выпадающий список combobox 'Класс объекта учета'. - """ - logger.info("Открытие combobox 'Класс объекта учета'...") - - combobox_locator = self.page.locator(ComboboxLocators.OBJECT_CLASS_COMBOBOX) - listbox_locator = self.page.locator(ComboboxLocators.LISTBOX_SELECTOR) - icon_locator = combobox_locator.locator(ComboboxLocators.COMBOBOX_ICON) - - # Прокручиваем до combobox - combobox_locator.scroll_into_view_if_needed() - self.wait_for_timeout(1000) - - # Проверяем, не открыт ли уже список - 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.scroll_into_view_if_needed() - 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(ComboboxLocators.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(ComboboxLocators.OBJECT_CLASS_COMBOBOX) - - selected_value = "" - - # Ищем в span элементах - span_locator = combobox_locator.locator(ComboboxLocators.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: """ Проверяет что выбран указанный класс объекта. @@ -365,8 +554,6 @@ class CreateRackElementTab(BasePage): logger.info(f"Элемент '{item_text}' присутствует в списке") - # =============== МЕТОДЫ ДЛЯ РАБОТЫ СО СТОЙКОЙ ======================== - def check_rack_fields_presence(self) -> None: """ Проверяет наличие полей специфичных для стойки. @@ -378,21 +565,21 @@ class CreateRackElementTab(BasePage): # Основные обязательные поля required_fields = [ - (RACK_NAME_FIELD, "Имя"), - (RACK_HEIGHT_FIELD, "Высота в юнитах"), - (RACK_DEPTH_FIELD, "Глубина (мм)") + (RackLocators.RACK_NAME_FIELD, "Имя"), + (RackLocators.RACK_HEIGHT_FIELD, "Высота в юнитах"), + (RackLocators.RACK_DEPTH_FIELD, "Глубина (мм)") ] # Дополнительные поля optional_fields = [ - (RACK_SERIAL_FIELD, "Серийный номер"), - (RACK_INVENTORY_FIELD, "Инвентарный номер"), - (RACK_COMMENT_FIELD, "Комментарий"), - (RACK_CABLE_ENTRY_FIELD, "Ввод кабеля"), - (RACK_STATE_FIELD, "Состояние"), - (RACK_OWNER_FIELD, "Владелец"), - (RACK_SERVICE_ORG_FIELD, "Обслуживающая организация"), - (RACK_PROJECT_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, "Проект/Титул") ] # Проверяем обязательные поля @@ -410,250 +597,86 @@ class CreateRackElementTab(BasePage): logger.info("Все основные поля для стойки присутствуют") - def fill_rack_data(self, name: str, height: str = "42", depth: str = "1000", - serial: str = "", inventory: str = "", comment: str = "", - cable_entry: str = "", state: str = "", owner: str = "", - service_org: str = "", project: str = "") -> None: + def check_field_highlighted_error(self, field_name: str) -> None: """ - Заполняет данные для создания стойки. + Проверяет, что поле подсвечено цветом ошибки (валидация не пройдена). Args: - name: Наименование стойки - height: Высота в юнитах (по умолчанию 42) - depth: Глубина в мм (по умолчанию 1000) - serial: Серийный номер - inventory: Инвентарный номер - comment: Комментарий - cable_entry: Ввод кабеля - state: Состояние - owner: Владелец - service_org: Обслуживающая организация - project: Проект/Титул + field_name: Название поля для проверки """ - logger.info(f"Заполнение данных стойки: {name}") + logger.info(f"Проверка подсветки поля '{field_name}' цветом ошибки...") - # Заполняем обязательные поля - name_field = self.page.locator(RACK_NAME_FIELD) - name_field.fill(name) - logger.info(f"Заполнено поле 'Имя': {name}") + # Локаторы только для обязательных полей + required_fields = { + "Имя": RackLocators.RACK_NAME_FIELD, + "Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD, + "Глубина (мм)": RackLocators.RACK_DEPTH_FIELD + } - self._select_combobox("Высота в юнитах", height) - logger.info(f"Выбрана высота: {height} юнитов") + if field_name not in required_fields: + raise ValueError(f"Поле '{field_name}' не является обязательным или не поддерживается") - self._select_combobox("Глубина (мм)", depth) - logger.info(f"Выбрана глубина: {depth} мм") - - # Заполняем опциональные поля - if serial: - serial_field = self.page.locator(RACK_SERIAL_FIELD) - serial_field.fill(serial) - logger.info(f"Заполнен серийный номер: {serial}") - - if inventory: - inventory_field = self.page.locator(RACK_INVENTORY_FIELD) - inventory_field.fill(inventory) - logger.info(f"Заполнен инвентарный номер: {inventory}") - - if comment: - comment_field = self.page.locator(RACK_COMMENT_FIELD) - comment_field.fill(comment) - logger.info(f"Добавлен комментарий: {comment}") - - # Заполняем дополнительные combobox поля - if cable_entry: - self._select_combobox("Ввод кабеля", cable_entry) - logger.info(f"Выбран ввод кабеля: {cable_entry}") - - if state: - self._select_combobox("Состояние", state) - logger.info(f"Выбрано состояние: {state}") - - if owner: - self._select_combobox("Владелец", owner) - logger.info(f"Выбран владелец: {owner}") - - if service_org: - self._select_combobox("Обслуживающая организация", service_org) - logger.info(f"Выбрана обслуживающая организация: {service_org}") - - if project: - self._select_combobox("Проект/Титул", project) - logger.info(f"Выбран проект/титул: {project}") - - logger.info("Данные стойки заполнены") - - def _select_combobox(self, field_name: str, value: str) -> None: - """ - Выбор значения в combobox. - - Args: - field_name: Название поля - value: Значение для выбора - """ - logger.info(f"Выбор '{value}' в поле '{field_name}'...") - - # Получаем статический локатор из словаря - if field_name not in COMBOBOX_FIELDS_MAP: - raise ValueError(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP") - - field_locator = COMBOBOX_FIELDS_MAP[field_name] - field_container = self.page.locator(field_locator) - - # Прокручиваем до поля - field_container.scroll_into_view_if_needed() - self.wait_for_timeout(500) - - # Проверяем видимость поля - self.base_component.check_visibility(field_container, f"Поле '{field_name}' не найдено") - - # Кликаем на контейнер чтобы активировать поле - field_container.click() - self.wait_for_timeout(1000) - - # Вводим значение - self.page.keyboard.type(value) - self.wait_for_timeout(500) - self.page.keyboard.press("Enter") - - logger.info(f"Поле '{field_name}' заполнено") - - def create_rack(self, rack_name: str, **kwargs) -> None: - """ - Полный процесс создания стойки. - - Args: - rack_name: Наименование стойки - **kwargs: Дополнительные параметры стойки - """ - logger.info(f"Начало процесса создания стойки: {rack_name}") - - # Выбираем класс объекта "Стойка" - self.select_object_class("Стойка") - self.wait_for_timeout(1000) - - # Проверяем наличие полей стойки - self.check_rack_fields_presence() - - # Заполняем данные - self.fill_rack_data(rack_name, **kwargs) - - # Создаем стойку - self.click_add_button() - - logger.info(f"Процесс создания стойки '{rack_name}' завершен") - - def clear_name_field(self) -> None: - """ - Очищает поле 'Имя'. - """ - logger.info("Очистка поля 'Имя'...") - name_field = self.page.locator(RACK_NAME_FIELD) - name_field.fill("") - logger.info("Поле 'Имя' очищено") - - def check_rack_exists(self, rack_name: str) -> bool: - """ - Проверяет, существует ли уже стойка с указанным именем в навигационной панели. - - Args: - rack_name: Имя стойки для проверки - - Returns: - bool: True если стойка существует, False если нет - """ - logger.info(f"Проверка существования стойки с именем '{rack_name}'") - - self.main_page.click_main_navigation_panel_item("Объекты") - self.main_page.click_main_navigation_panel_item("Объекты") - self.wait_for_timeout(1000) - self.main_page.click_subpanel_item("test-zone") - self.wait_for_timeout(1000) - - # Используем TREEVIEW локатор из NavigationPanelLocators - nav_panel_locator = NavigationPanelLocators.TREEVIEW - - # Проверяем видимость элемента через is_visible - element = self.page.locator(nav_panel_locator).get_by_text(rack_name).first - - if element.is_visible(): - logger.info(f"Стойка с именем '{rack_name}' найдена") - return True - else: - logger.info(f"Стойки с именем '{rack_name}' не найдена") - return False - - def clear_combobox_field(self, field_name: str) -> None: - """ - Очищает значение в combobox поле с помощью кнопки закрытия (крестика). - - Args: - field_name: Название поля для очистки - """ - logger.info(f"Очистка combobox поля '{field_name}' с помощью кнопки закрытия...") - - if field_name not in COMBOBOX_FIELDS_MAP: - logger.warning(f"Локатор для поля '{field_name}' не найден в COMBOBOX_FIELDS_MAP") - return - - field_locator = COMBOBOX_FIELDS_MAP[field_name] - - # Находим поле по локатору - field_container = self.page.locator(field_locator).first + field_locator = required_fields[field_name] + field_element = self.page.locator(field_locator) # Проверяем что поле видимо - if not field_container.is_visible(): - logger.info(f"Поле '{field_name}' не видимо, пропускаем очистку") - return + self.base_component.check_visibility(field_element, f"Поле '{field_name}' не найдено") - # Прокручиваем до поля - field_container.scroll_into_view_if_needed() - self.wait_for_timeout(500) + # Ищем родительский контейнер с использованием константы + parent_container = field_element.locator(RackLocators.INPUT_PARENT_CONTAINER).first - # Ищем кнопку закрытия (крестик) внутри контейнера поля - close_button = field_container.locator(ComboboxLocators.COMBOBOX_CLOSE_BUTTON) + # Проверка классов ошибки + if parent_container.count() > 0: + error_classes = AlertLocators.ERROR_CLASSES - # Проверяем наличие и видимость кнопки закрытия - if close_button.count() > 0 and close_button.is_visible(): - # Если кнопка закрытия видима - кликаем на нее - close_button.click() - self.wait_for_timeout(500) - logger.info(f"Combobox поле '{field_name}' очищено с помощью кнопки закрытия") - else: - # Если кнопки закрытия нет, просто логируем этот факт - logger.info(f"Кнопка закрытия не найдена для поля '{field_name}', очистка не выполнена") + is_error_highlighted = False + for error_class in error_classes: + error_element = parent_container.locator(f".{error_class}") + if error_element.count() > 0: + is_error_highlighted = True + logger.info(f"Поле '{field_name}' подсвечено ошибкой") + break - def clear_rack_fields(self) -> None: + if not is_error_highlighted: + raise AssertionError(f"Поле '{field_name}' не подсвечено цветом ошибки ") + + logger.info(f"Поле '{field_name}' корректно подсвечено цветом ошибки") + + def check_field_not_highlighted_error(self, field_name: str) -> None: """ - Очищает все поля формы создания стойки. + Проверяет, что поле НЕ подсвечено цветом ошибки (валидация успешна). + + Args: + field_name: Название поля для проверки """ - logger.info("Очистка всех полей формы стойки...") + logger.info(f"Проверка отсутствия подсветки ошибки у поля '{field_name}'...") - # Очищаем текстовые поля - text_fields = [ - (RACK_NAME_FIELD, "Имя"), - (RACK_SERIAL_FIELD, "Серийный номер"), - (RACK_INVENTORY_FIELD, "Инвентарный номер"), - (RACK_COMMENT_FIELD, "Комментарий") - ] + # Локаторы только для обязательных полей + required_fields = { + "Имя": RackLocators.RACK_NAME_FIELD, + "Высота в юнитах": RackLocators.RACK_HEIGHT_FIELD, + "Глубина (мм)": RackLocators.RACK_DEPTH_FIELD + } - for field_locator, field_name in text_fields: - field = self.page.locator(field_locator) - if field.count() > 0 and field.first.is_visible(): - field.fill("") - logger.info(f"Текстовое поле '{field_name}' очищено") + if field_name not in required_fields: + raise ValueError(f"Поле '{field_name}' не является обязательным или не поддерживается") - # Очищаем combobox поля - combobox_fields = [ - "Высота в юнитах", - "Глубина (мм)", - "Ввод кабеля", - "Состояние", - "Владелец", - "Обслуживающая организация", - "Проект/Титул" - ] + field_locator = required_fields[field_name] + field_element = self.page.locator(field_locator) - for field_name in combobox_fields: - self.clear_combobox_field(field_name) + # Проверяем что поле видимо + self.base_component.check_visibility(field_element, f"Поле '{field_name}' не найдено") - logger.info("Все поля формы стойки очищены") + # Ищем родительский контейнер с использованием константы + parent_container = field_element.locator(RackLocators.INPUT_PARENT_CONTAINER).first + + # Поверка отсутствия классов ошибки + if parent_container.count() > 0: + error_classes = AlertLocators.ERROR_CLASSES + + for error_class in error_classes: + error_element = parent_container.locator(f".{error_class}") + if error_element.count() > 0: + raise AssertionError(f"Поле '{field_name}' подсвечено ошибкой") + + logger.info(f"Поле '{field_name}' корректно не подсвечено цветом ошибки") diff --git a/tests/e2e/create_elements/__pycache__/test_create_child_element.cpython-313-pytest-8.4.1.pyc b/tests/e2e/create_elements/__pycache__/test_create_child_element.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 8c72922d816396ed68b2429d0eecbfc0fc3b4f99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9111 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
Rb65wSVkdgiBhH~r?3*rNP^Q= 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 je8sI}`3L^R*PsL=*L@q`@cf781-C None: - """Тест создания дочернего элемента типа 'Стойка'.""" - # Создаем экземпляр страницы и переходим к созданию дочернего элемента child_element_page = CreateChildElementTab(browser) child_element_page.click_create_button() @@ -55,6 +44,10 @@ class TestCreateRackElement: child_element_page.check_object_class_selected("Стойка") child_element_page.wait_for_timeout(2000) + #@pytest.mark.develop + def test_create_rack_content(self, browser: Page) -> None: + """Тест создания дочернего элемента типа 'Стойка'.""" + rack_element_page = CreateRackElementTab(browser) # Проверяем заголовок формы создания @@ -66,17 +59,9 @@ class TestCreateRackElement: rack_element_page.should_be_toolbar_buttons() - #@pytest.mark.develop def test_create_rack_child_element(self, browser: Page) -> None: """Тест создания дочернего элемента типа 'Стойка'.""" - # Создаем экземпляр страницы и переходим к созданию дочернего элемента - child_element_page = CreateChildElementTab(browser) - child_element_page.click_create_button() - child_element_page.select_object_class("Стойка") - child_element_page.check_object_class_selected("Стойка") - child_element_page.wait_for_timeout(2000) - rack_element_page = CreateRackElementTab(browser) # Проверяем что после выбора 'Стойка' появляются специфичные поля @@ -92,7 +77,9 @@ class TestCreateRackElement: depth="1000", serial="TEST123456", inventory="INV-001", - comment="Тестовая стойка для автоматизации" + comment="Тестовая стойка для автоматизации", + cable_entry="Сверху", + state="В эксплуатации" ) # Нажимаем кнопку создания @@ -102,7 +89,6 @@ class TestCreateRackElement: logger.info("Тест создания дочернего элемента 'Стойка' завершен успешно") - #@pytest.mark.develop def test_create_rack_with_duplicate_name(self, browser: Page) -> None: """ Тест создания стойки с уже существующим именем. @@ -113,7 +99,9 @@ class TestCreateRackElement: logger.info("Запуск теста создания стойки с дублирующимся именем") rack_name = "Test-Rack-01" + rack_element_page = CreateRackElementTab(browser) + rack_element_page.click_cancel_button() # Проверяем, существует ли уже стойка с таким именем if not rack_element_page.check_rack_exists(rack_name): @@ -138,12 +126,12 @@ class TestCreateRackElement: # Создаем первую стойку rack_element_page.click_add_button() - rack_element_page.wait_for_timeout(3000) + rack_element_page.wait_for_timeout(2000) logger.info(f"Первая стойка с именем '{rack_name}' успешно создана") else: logger.info(f"Стойка с именем '{rack_name}' уже существует, переходим к созданию второй") - # Теперь пытаемся создать вторую стойку с тем же именем + # Cоздаем вторую стойку с тем же именем logger.info(f"Пытаемся создать вторую стойку с именем '{rack_name}'") # Переходим обратно к созданию новой стойки @@ -171,20 +159,16 @@ class TestCreateRackElement: # Проверяем наличие alert-окна с сообщением о дублирующемся имени expected_alert_text = f"Имя {rack_name} уже используется" rack_element_page.alert.check_alert_presence(expected_alert_text) - logger.info(f"Alert-окно с текстом '{expected_alert_text}' успешно отображено") # Проверяем, что остались на странице создания (стойка не создана) rack_element_page.check_toolbar_title('Создать дочерний элемент в') + + # Закрываем alert-окно с помощью кнопки закрытия + rack_element_page.wait_for_timeout(2000) + rack_element_page.alert.close_alert_by_text(expected_alert_text) + logger.info("Система не позволила создать стойку с дублирующимся именем") - # Проверяем исчезновение alert-окна через некоторое время - rack_element_page.wait_for_timeout(5000) - rack_element_page.alert.check_alert_absence(expected_alert_text, timeout=20000) - logger.info("Alert-окно успешно исчезло") - - logger.info("Тест создания стойки с дублирующимся именем завершен успешно") - - @pytest.mark.develop def test_required_fields_validation(self, browser: Page) -> None: """ Тест проверки обязательных полей при создании стойки. @@ -194,33 +178,35 @@ class TestCreateRackElement: - Поле 'Высота в юнитах' должно быть заполнено - Поле 'Глубина (мм)' должно быть заполнено """ + # Текст сообщения alert-окна - #expected_alert_text_name = f"поле Имя должно быть заполнено" # Ошибка, поле не отслеживается + expected_alert_text_name = f"поле Имя должно быть заполнено" expected_alert_text_height = f"поле Высота в юнитах должно быть заполнено" expected_alert_text_depth = f"поле Глубина (мм) должно быть заполнено" - - # Создаем экземпляр страницы и переходим к созданию дочернего элемента - child_element_page = CreateChildElementTab(browser) - child_element_page.click_create_button() - child_element_page.select_object_class("Стойка") - child_element_page.check_object_class_selected("Стойка") - logger.info("Класс объекта 'Стойка' успешно выбран") - child_element_page.wait_for_timeout(2000) - rack_element_page = CreateRackElementTab(browser) # Проверяем наличие полей стойки rack_element_page.check_rack_fields_presence() - logger.info("Специфичные поля для стойки отображаются корректно") # 1. Тест: Попытка создания стойки поля - default - logger.info("Тест 1: Попытка создания стойки без заполнения обязательных полей") + logger.info("Тест 1: Создание стойки заполнене полей - default") # Нажимаем кнопку создания без заполнения данных rack_element_page.click_add_button() rack_element_page.wait_for_timeout(2000) + # Проверяем подсветку обязательных полей 'Высота в юнитах' и 'Глубина (мм)' цветом ошибки + #rack_element_page.check_field_not_highlighted_error("Имя") + rack_element_page.check_field_highlighted_error("Высота в юнитах") + rack_element_page.check_field_highlighted_error("Глубина (мм)") + + logger.info("Проверка валидации поля 'Имя' временно отключена - ожидаем фикс от разработчика") + # Проверяем наличие alert-окна с сообщением о заполнении Имя + #rack_element_page.alert.check_alert_presence(expected_alert_text_name) + # Закрываем alert-окно Имя с помощью кнопки закрытия + #rack_element_page.alert.close_alert_by_text(expected_alert_text_name) + # Проверяем наличие alert-окна с сообщением о заполнении Высота в юнитах rack_element_page.alert.check_alert_presence(expected_alert_text_height) # Закрываем alert-окно Высота в юнитах с помощью кнопки закрытия @@ -249,10 +235,16 @@ class TestCreateRackElement: rack_element_page.click_add_button() rack_element_page.wait_for_timeout(2000) + # Проверяем подсветку всех обязательных полей цветом ошибки + #rack_element_page.check_field_highlighted_error("Имя") + rack_element_page.check_field_highlighted_error("Высота в юнитах") + rack_element_page.check_field_highlighted_error("Глубина (мм)") + + logger.info("Проверка валидации поля 'Имя' временно отключена - ожидаем фикс от разработчика") # Проверяем наличие alert-окна с сообщением о заполнении Имя - #rack_element_page.alert.check_alert_presence(expected_alert_text_name) # Ошибка, поле не отслеживается + #rack_element_page.alert.check_alert_presence(expected_alert_text_name) # Закрываем alert-окно Имя с помощью кнопки закрытия - #rack_element_page.alert.close_alert_by_text(expected_alert_text_name) # Ошибка, поле не отслеживается + #rack_element_page.alert.close_alert_by_text(expected_alert_text_name) # Проверяем наличие alert-окна с сообщением о заполнении Высота в юнитах rack_element_page.alert.check_alert_presence(expected_alert_text_height) @@ -267,6 +259,7 @@ class TestCreateRackElement: # Проверяем, что остались на той же странице rack_element_page.check_toolbar_title('Создать дочерний элемент в') logger.info("Система не позволила создать стойку без имени, высоты и глубины") + rack_element_page.wait_for_timeout(2000) # 3. Тест: Заполняем только поле 'Высота в юнитах' logger.info("Тест 3: Заполняем только поле 'Высота в юнитах'") @@ -285,10 +278,13 @@ class TestCreateRackElement: rack_element_page.click_add_button() rack_element_page.wait_for_timeout(2000) - # Проверяем наличие alert-окна с сообщением о заполнении Имя - #rack_element_page.alert.check_alert_presence(expected_alert_text_name) # Ошибка, поле не отслеживается - # Закрываем alert-окно Имя с помощью кнопки закрытия - #rack_element_page.alert.close_alert_by_text(expected_alert_text_name) # Ошибка, поле не отслеживается + # Проверяем подсветку полей 'Имя' и 'Глубина (мм)' цветом ошибки + #rack_element_page.check_field_highlighted_error("Имя") + rack_element_page.check_field_not_highlighted_error("Высота в юнитах") + rack_element_page.check_field_highlighted_error("Глубина (мм)") + + # Проверяем, что НЕТ alert-окна для поля 'Высота в юнитах' (оно заполнено) + rack_element_page.alert.check_alert_absence(expected_alert_text_height, 1000) # Проверяем наличие alert-окна с сообщением о заполнении Глубины (мм) rack_element_page.alert.check_alert_presence(expected_alert_text_depth) @@ -298,6 +294,7 @@ class TestCreateRackElement: # Проверяем, что остались на той же странице rack_element_page.check_toolbar_title('Создать дочерний элемент в') logger.info("Система не позволила создать стойку без имени и глубины") + rack_element_page.wait_for_timeout(2000) # 4. Тест: Заполняем только поле 'Глубина (мм)' logger.info("Тест 4: Заполняем только поле 'Глубина (мм)'") @@ -316,10 +313,15 @@ class TestCreateRackElement: rack_element_page.click_add_button() rack_element_page.wait_for_timeout(2000) - # Проверяем наличие alert-окна с сообщением о заполнении Имя - #rack_element_page.alert.check_alert_presence(expected_alert_text_name) # Ошибка, поле не отслеживается - # Закрываем alert-окно Имя с помощью кнопки закрытия - #rack_element_page.alert.close_alert_by_text(expected_alert_text_name) # Ошибка, поле не отслеживается + # Проверяем подсветку полей 'Имя' и 'Высота в юнитах' цветом ошибки + #rack_element_page.check_field_highlighted_error("Имя") + rack_element_page.check_field_highlighted_error("Высота в юнитах") + rack_element_page.check_field_not_highlighted_error("Глубина (мм)") + + logger.info("Проверка отсутствия alert-окна для поля 'Глубина (мм)' временно отключена - ожидаем фикс от разработчика") + # Проверяем, что НЕТ alert-окна для поля 'Глубина (мм)' (оно заполнено) + #rack_element_page.alert.check_alert_absence(expected_alert_text_depth, 1000) + #logger.info("Alert-окно для поля 'Глубина (мм)' не появилось - поле заполнено корректно") # Проверяем наличие alert-окна с сообщением о заполнении Высота в юнитах rack_element_page.alert.check_alert_presence(expected_alert_text_height) @@ -328,7 +330,7 @@ class TestCreateRackElement: # Проверяем, что остались на той же странице rack_element_page.check_toolbar_title('Создать дочерний элемент в') - logger.info("Система не позволила создать стойку без высоты") + logger.info("Система не позволила создать стойку без имени и высоты") rack_element_page.wait_for_timeout(2000) # 5. Тест: Заполняем все обязательные поля @@ -344,12 +346,24 @@ class TestCreateRackElement: depth="1000" ) + # Проверяем, что ни одно поле не подсвечено цветом ошибки (все заполнены корректно) + rack_element_page.check_field_not_highlighted_error("Имя") + rack_element_page.check_field_not_highlighted_error("Высота в юнитах") + rack_element_page.check_field_not_highlighted_error("Глубина (мм)") + logger.info("Ни одно обязательное поле не подсвечено цветом ошибки - все поля заполнены корректно") + # Нажимаем кнопку создания rack_element_page.click_add_button() # Ждем завершения создания (должны перейти на другую страницу) rack_element_page.wait_for_timeout(3000) + # Проверяем, что НЕТ alert-окон для всех обязательных полей (все заполнены корректно) + rack_element_page.alert.check_alert_absence(expected_alert_text_name, 1000) + rack_element_page.alert.check_alert_absence(expected_alert_text_height, 1000) + rack_element_page.alert.check_alert_absence(expected_alert_text_depth, 1000) + logger.info("Alert-окна для обязательных полей не появились - все поля заполнены корректно") + # Проверяем, что ушли со страницы создания (косвенная проверка успешного создания) try: rack_element_page.check_toolbar_title('Создать дочерний элемент в') @@ -358,5 +372,3 @@ class TestCreateRackElement: logger.info("Страница создания закрыта - стойка успешно создана") logger.info("Тест проверки обязательных полей завершен успешно") - - diff --git a/tests/e2e/rack/__pycache__/test_create_rack.cpython-313-pytest-8.4.1.pyc b/tests/e2e/rack/__pycache__/test_create_rack.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index ca13e96699229315c09f6145e61b58296bf65b33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5769 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 Vdmc*LPVL}AirxSK diff --git a/tests/e2e/rack/__pycache__/test_rack_general_info.cpython-313-pytest-8.4.1.pyc b/tests/e2e/rack/__pycache__/test_rack_general_info.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 3c66cff4229a2e3b68f30a7b12876befba4df808..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3030 zcmb_eTWnNC7@oaPce`y7K`61DZh*Q0)v9AMmNLCA|92oV~SO<;9ci%s>Bi=Ktsa zX3N2#AHl~5T-q-{{V5jJ&s`zPBS4%*F~m^J5p(uAm{ZQKUbpU4P_Ku16Gd<_r);|gdLXqLp4od_C?j?p@V7y$d>o=X)Rab~Co+!JkWRJZA9+iG6S6~{4m%(L4a^LC<7-Fqr3tR?1S z{2ZFUgu*heDKkpfE~fEqKZNTomJC zJ=5kC_;vfP1WXTLKlbXI)TWGiT} zTF^!-SHCOjTVVprx)sP*c*0d&2{&j6x%3o4^IL+j{4DC{fl7ET0ei>8G(hN!bZ|0LR1QUJEHH%n4- z4e$Z5)||3}$zeLe@;Sn^^svFim13f1OdQvyL}?F%{Y*5Jg;RNw5n#%^V%ez==Zl#X zNl=ngN3^tRXyB`$=4i(17*#cc4CNUyv@FdR4NFO8wB#X@1^iv5PBepNt+lp>UQ9?I zlF7uwXsxeD8m4NBvPzV$IzN#&hKa7Fs7_LvUdX6pG!^z*Zk=X^n0R+BXQp5|vjwY@ zepaAKgQkd)&u0=UBM{Nhj11K+Co^<$Hbs6*KLh#ZEAfF3qJ42dXmNQ;@(qJ}-4WO; zxEJ{aQJi!9QXKMpUeqtrtQ_%o`rnQ4(|lT@>Wo-7_~ifdh~8^^9ko~*gqBcB5*3W_O;edq+T<9z%#PSo8&8LbPLAMfexTt0-l6FFdG^QIFEk|>C5c>#8 z<&!{o3u7YhmOHDmLzXW^M`$KrVB&pan_;ll5uzJV?j%tSgK3E(s1w3MLZMp>U1Gr1 zL3FIX3x;Fpfz#!|ix_!Zj}J}^E_gPWo(+r6ou1GSsYL`+$ta7d>_Xm+Wx==o30L8# z7=R13e4?mj3@xWyt#1^@NNu%|*xQb%Iy~i~@gt1FMK!3^Qe#=ZD$ftb@^-0=JtzFS z1uR%=>(@J?kLBQm1x=pZ+E{@9ae72v*ap&26E-4r*i4B=00wDlBUHQ1mLMa!qM7a7 zEYFvP_t}+to$Un`@y|lv0mGup;cz@~x*hJve&lc*MMr|aqrhKi%RRL79@=&fZF%h7 MXJ%>o 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 diff --git a/tests/e2e/sessions/__pycache__/test_session_settings_tab.cpython-313-pytest-8.4.1.pyc b/tests/e2e/sessions/__pycache__/test_session_settings_tab.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index d2ffe62a6a21f1a334775b9a84db854802b23114..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9950 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 diff --git a/tests/e2e/users/__pycache__/__init__.cpython-313.pyc b/tests/e2e/users/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index d7e74a6c34951abd7a64e2f22dbfa54f54cd9f30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 322 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 XZ-s8_Z_j6L*={>-|3u&gKc4>uaHBNR diff --git a/tests/e2e/users/__pycache__/test_edit_user.cpython-313-pytest-8.4.1.pyc b/tests/e2e/users/__pycache__/test_edit_user.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index a8d6b24bcf31975aa37cfaeca2854c8c9fb55a36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11345 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 PZ!w)UeZ{eaE!6)4)huzi diff --git a/tests/e2e/users/__pycache__/test_user_card.cpython-313-pytest-8.4.1.pyc b/tests/e2e/users/__pycache__/test_user_card.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 543edc77e41900e0f563cd18b7fee49617e77bfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12843 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