From a5c73285628801b61738a67c1ff310b100b2e723 Mon Sep 17 00:00:00 2001 From: nsubbot Date: Mon, 24 Nov 2025 14:00:55 +0300 Subject: [PATCH 1/9] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B0=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4?= =?UTF-8?q?=D0=BA=D0=B8=20'=D0=A1=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../container_system_log_events.py | 6 ++--- tests/e2e/test_system_log_events_container.py | 25 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/components_derived/container_system_log_events.py b/components_derived/container_system_log_events.py index d654dc2..34f977c 100644 --- a/components_derived/container_system_log_events.py +++ b/components_derived/container_system_log_events.py @@ -67,13 +67,13 @@ class SystemLogEventsContainer(EventsContainerComponent): sidebar_filter.check_content() filter_type_bar = sidebar_filter.get_filtering_parameter("filter_type") - filter_type_title = filter_type_bar.get_filter_parameter_title() + filter_type_title = filter_type_bar.get_selection_bar_title() assert filter_type_title == "Тип", "Filtering parameter bar 'Тип' is missing" filter_strictness_bar = sidebar_filter.get_filtering_parameter("filter_strictness") - filter_strictness_title = filter_strictness_bar.get_filter_parameter_title() + filter_strictness_title = filter_strictness_bar.get_selection_bar_title() assert filter_strictness_title == "Строгость", "Filtering parameter bar 'Строгость' is missing" filter_host_bar = sidebar_filter.get_filtering_parameter("filter_host") - filter_host_title = filter_host_bar.get_filter_parameter_title() + filter_host_title = filter_host_bar.get_selection_bar_title() assert filter_host_title == "Хост", "Filtering parameter bar 'Хост' is missing" diff --git a/tests/e2e/test_system_log_events_container.py b/tests/e2e/test_system_log_events_container.py index 9a3c653..de6da92 100644 --- a/tests/e2e/test_system_log_events_container.py +++ b/tests/e2e/test_system_log_events_container.py @@ -4,7 +4,7 @@ контейнера для отображения событий системного журнала. """ -# import pytest +import pytest from playwright.sync_api import Page from pages.main_page import MainPage from pages.login_page import LoginPage @@ -154,7 +154,7 @@ class TestSystemLogEventsContainer: state = system_log_events_container.get_arrow_button_state(index) assert state == "up", "Arrow button should be up" is_descending_order = system_log_events_container.check_events_table_column_descending_order(index, - convert2timestamp=True) + convert2timestamp=True) assert not is_descending_order, "Column data should be in ascending order" system_log_events_container.click_event_table_header_arrow(index) @@ -162,7 +162,7 @@ class TestSystemLogEventsContainer: state = system_log_events_container.get_arrow_button_state(index) assert state == "down", "Arrow button should be down" is_descending_order = system_log_events_container.check_events_table_column_descending_order(index, - convert2timestamp=True) + convert2timestamp=True) assert is_descending_order, "Column data should be in descending order" # @pytest.mark.develop @@ -237,7 +237,8 @@ class TestSystemLogEventsContainer: system_log_events_container.should_be_final_state() current_number = system_log_events_container.get_current_data_set_number() - assert current_number == last_number, f"Expected page number {last_number} is not equal actual {current_number}" + assert current_number == last_number, \ + f"Expected page number {last_number} is not equal actual {current_number}" # Переход на первую страницу, переход на следующую страницу, возврат на первую страницу system_log_events_container.click_first_page() @@ -272,15 +273,18 @@ class TestSystemLogEventsContainer: # to_do: проверка, что происходит обновление содержимого таблицы counter = 1 - while counter <= tested_pages: + while counter < tested_pages: system_log_events_container.click_chevron_right() + counter += 1 browser.wait_for_timeout(2000) - system_log_events_container.should_be_all_enabled() - - counter += 1 + if counter == tested_pages: + system_log_events_container.should_be_final_state() + else: + system_log_events_container.should_be_all_enabled() current_number = system_log_events_container.get_current_data_set_number() - assert current_number == counter, f"Expected page number {counter} is not equal actual {current_number}" + assert current_number == counter,\ + f"Expected page number {counter} is not equal actual {current_number}" # загрузка страниц от конца к началу # to_do: проверка, что происходит обновление содержимого таблицы @@ -292,7 +296,8 @@ class TestSystemLogEventsContainer: counter -= 1 current_number = system_log_events_container.get_current_data_set_number() - assert current_number == counter, f"Expected page number {counter} is not equal actual {current_number}" + assert current_number == counter,\ + f"Expected page number {counter} is not equal actual {current_number}" # Проверка возврата к начальному состоянию system_log_events_container.click_chevron_left() From 5cb1cfdc38f5ef20267f37c421f054083c1986da Mon Sep 17 00:00:00 2001 From: Radislav Date: Tue, 25 Nov 2025 10:33:34 +0300 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4?= =?UTF-8?q?=D0=BE=D0=B2=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20?= =?UTF-8?q?combobox=20=D0=B2=20dropdown=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены методы open_combobox, get_combobox_options, get_selected_combobox_value - Улучшено логирование и обработка состояний combobox --- components/dropdown_list_component.py | 128 +++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 5 deletions(-) diff --git a/components/dropdown_list_component.py b/components/dropdown_list_component.py index 160aa47..9df5267 100644 --- a/components/dropdown_list_component.py +++ b/components/dropdown_list_component.py @@ -17,7 +17,7 @@ class DropdownList(BaseComponent): методы для выбора и проверки элементов списка. """ - def __init__(self, page: Page): + def __init__(self, page: Page) -> None: """Инициализирует компонент выпадающего списка. Args: @@ -37,9 +37,39 @@ class DropdownList(BaseComponent): element = self.page.get_by_role("listitem").filter(has_text=text) if element.count() > 1: rtext = f"^{text}$" - element = self.page.get_by_role("listitem").filter(has_text=re.compile(rtext)) + element = self.page.get_by_role("listitem").filter( + has_text=re.compile(rtext) + ) element.click() + 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("Getting combobox options list...") + + # Открываем 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"Found options: {len(options_list)} - {options_list}") + return options_list + def get_item_names(self, locator: str | Locator) -> list[str]: """Возвращает тексты всех элементов по указанному локатору. @@ -54,6 +84,90 @@ class DropdownList(BaseComponent): texts = loc.all_inner_texts() return texts[0].splitlines() + 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"Selected combobox value: '{selected_value}'") + return selected_value + + 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("Opening 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 click completed") + 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("Dropdown list found and opened") + else: + logger.warning("Failed to open dropdown list") + def scroll_until_end(self, locator: str | Locator) -> None: """ Скроллит список до тех пор, пока не перестанут подгружаться новые элементы. @@ -82,7 +196,9 @@ class DropdownList(BaseComponent): attempts = 0 last_item_name = item_names[current_count-1] - element = self.page.get_by_role("listitem").filter(has_text=last_item_name) + element = self.page.get_by_role("listitem").filter( + has_text=last_item_name + ) element.scroll_into_view_if_needed() self.page.wait_for_timeout(300) @@ -98,11 +214,13 @@ class DropdownList(BaseComponent): Raises: AssertionError: Если элемент отсутствует или недоступен. """ - + element = self.page.get_by_role("listitem").filter(has_text=text) if element.count() > 1: rtext = f"^{text}$" - element = self.page.get_by_role("listitem").filter(has_text=re.compile(rtext)) + element = self.page.get_by_role("listitem").filter( + has_text=re.compile(rtext) + ) enabled = element.is_enabled() if not enabled: assert False, f"Dropdown list item '{text}' is missing or disabled" From 13608f7e4bb7f90cfa9d649f25c8a1213ce78429 Mon Sep 17 00:00:00 2001 From: Radislav Date: Tue, 25 Nov 2025 10:43:17 +0300 Subject: [PATCH 3/9] =?UTF-8?q?refactor(alert):=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83?= =?UTF-8?q?=D1=80=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=20alert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен импорт модуля локаторов и использование AlertLocators для лучшей поддерживаемости - Реализован метод close_alert_by_text() с проверками видимости - Добавлено комплексное логирование для всех операций с alert-окнами - Улучшены подсказки типов с явными аннотациями возвращаемых значений - Обновлено использование логгера с consistent форматированием сообщений --- components/alert_component.py | 95 ++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/components/alert_component.py b/components/alert_component.py index 2bc1437..98547ba 100644 --- a/components/alert_component.py +++ b/components/alert_component.py @@ -6,6 +6,7 @@ alert-окон (error, success, info, warning) и проверки их сост from playwright.sync_api import Page, expect from tools.logger import get_logger +from locators.alert_locators import AlertLocators from elements.text_element import Text from components.base_component import BaseComponent @@ -19,7 +20,7 @@ class AlertComponent(BaseComponent): Позволяет проверять наличие, отсутствие и текст сообщений. """ - def __init__(self, page: Page): + def __init__(self, page: Page) -> None: """Инициализирует компонент alert-окна. Args: @@ -28,9 +29,41 @@ class AlertComponent(BaseComponent): super().__init__(page) - self.text = Text(page, "//div[contains(@class,'v-alert')]/div", "Alert message") + self.text = Text(page, AlertLocators.ALERT_MESSAGE, "Alert message") # Действия: + def close_alert_by_text(self, text: str) -> None: + """Закрывает alert-окно с заданным текстом с помощью кнопки закрытия. + + Args: + text: Текст alert-окна, которое нужно закрыть. + + Raises: + AssertionError: Если не удалось найти или закрыть alert-окно. + """ + # Находим alert с нужным текстом + alert_locator = self.page.get_by_role( + AlertLocators.ALERT_ROLE + ).filter(has_text=text) + + # Проверяем, что alert видим + expect(alert_locator).to_be_visible() + + # Находим кнопку закрытия внутри alert + close_button = alert_locator.locator(AlertLocators.ALERT_DISMISS_BUTTON) + + # Проверяем, что кнопка закрытия доступна и кликаем + expect(close_button).to_be_visible() + expect(close_button).to_be_enabled() + + # Кликаем по кнопке закрытия + close_button.click() + + # Проверяем, что alert исчез после закрытия + expect(alert_locator).to_be_hidden() + + logger.info(f"Alert with text '{text}' closed successfully") + def get_alert_type(self) -> str: """Возвращает тип alert-окна. @@ -41,7 +74,9 @@ class AlertComponent(BaseComponent): ValueError: Если получен неподдерживаемый тип alert-окна. """ - class_attr = self.page.get_by_role("alert").locator('>div').get_attribute('class') + class_attr = self.page.get_by_role(AlertLocators.ALERT_ROLE).locator( + '>div' + ).get_attribute('class') alert_type = None if 'v-alert' in class_attr: @@ -63,24 +98,7 @@ class AlertComponent(BaseComponent): return self.text.get_text(0) # Проверки: - def check_alert_presence(self, text: str): - """Проверяет наличие alert-окна с заданным текстом. - - Args: - text: Текст для проверки. Если пустая строка - проверяет только - наличие окна. - - Raises: - AssertionError: Если alert-окно не найдено. - """ - - msg = "Alert window is missing" - if text == "": - expect(self.page.get_by_role("alert")).to_be_visible(), msg - else: - expect(self.page.get_by_role("alert").filter(has_text=text)).to_be_visible(), msg - - def check_alert_absence(self, text: str, timeout: int = 30000): + def check_alert_absence(self, text: str, timeout: int = 30000) -> None: """Проверяет отсутствие alert-окна с заданным текстом. Args: @@ -93,9 +111,40 @@ class AlertComponent(BaseComponent): seconds = int(timeout/1000) msg = f"Alert window should disappear after {seconds} seconds" - expect(self.page.get_by_role("alert").filter(has_text=text)).to_be_hidden(timeout=timeout), msg - def check_text(self, alert_text: str): + if text == "": + expect(self.page.get_by_role( + AlertLocators.ALERT_ROLE + )).to_be_hidden(timeout=timeout), msg + logger.info(f"Alert window successfully disappeared") + else: + expect(self.page.get_by_role( + AlertLocators.ALERT_ROLE + ).filter(has_text=text)).to_be_hidden(timeout=timeout), msg + logger.info(f"Alert window with text '{text}' successfully disappeared") + + def check_alert_presence(self, text: str) -> None: + """Проверяет наличие alert-окна с заданным текстом. + + Args: + text: Текст для проверки. Если пустая строка - проверяет только + наличие окна. + + Raises: + AssertionError: Если alert-окно не найдено. + """ + + msg = "Alert window is missing" + if text == "": + expect(self.page.get_by_role(AlertLocators.ALERT_ROLE)).to_be_visible(), msg + logger.info(f"Alert window successfully displayed") + else: + expect(self.page.get_by_role( + AlertLocators.ALERT_ROLE + ).filter(has_text=text)).to_be_visible(), msg + logger.info(f"Alert window with text '{text}' successfully displayed") + + def check_text(self, alert_text: str) -> None: """Проверяет точное соответствие текста в alert-окне. Args: From a6eade473a9f6d309bebb970e80734fc06b4a013 Mon Sep 17 00:00:00 2001 From: Radislav Date: Tue, 25 Nov 2025 10:46:29 +0300 Subject: [PATCH 4/9] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F=20=D0=BB=D0=BE=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locators/alert_locators.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 locators/alert_locators.py diff --git a/locators/alert_locators.py b/locators/alert_locators.py new file mode 100644 index 0000000..12b1d86 --- /dev/null +++ b/locators/alert_locators.py @@ -0,0 +1,27 @@ +"""Модуль alert_locators содержит локаторы элементов alert-окон. + +Класс AlertLocators предоставляет XPath и CSS локаторы для взаимодействия +с alert-окнами (error, success, info, warning) в тестах. +""" + + +class AlertLocators: + """Локаторы элементов alert-окон. + + Содержит XPath и CSS локаторы для: + ALERT_ROLE (str): alert-окон по роли. + ALERT_BASE (str): базового контейнера alert-окон. + 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}}')]" + + # Классы для подсветки ошибок валидации полей + ERROR_CLASSES: list = ["error--text"] From 610d13575dead71305354724498fd370d9c90905 Mon Sep 17 00:00:00 2001 From: Radislav Date: Tue, 25 Nov 2025 13:56:24 +0300 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=82=D0=B8=D0=BF=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20=D0=BB=D0=BE=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B2=20toolbar=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлены типы параметров filter_text и locator - Обновлены аннотации типов методов - Улучшено форматирование кода --- components/toolbar_component.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/components/toolbar_component.py b/components/toolbar_component.py index e7d5a39..1e2c4e5 100644 --- a/components/toolbar_component.py +++ b/components/toolbar_component.py @@ -25,7 +25,7 @@ class ToolbarComponent(BaseComponent): title (str): Заголовок тулбара """ - def __init__(self, page: Page, title: str): + def __init__(self, page: Page, title: str) -> None: """Инициализирует компонент тулбара с указанным заголовком.""" super().__init__(page) self.title = title @@ -67,7 +67,8 @@ class ToolbarComponent(BaseComponent): """ self.buttons.append(Button(self.page, locator, name)) - def get_button_by_name(self, name: str) -> TooltipButton | TabButton | Button | None: + def get_button_by_name(self, name: str + ) -> TooltipButton | TabButton | Button | None: """Возвращает кнопку по имени. Args: @@ -95,12 +96,13 @@ class ToolbarComponent(BaseComponent): raise AssertionError(f"Unsupported button name {name}") button.click() - def get_toolbar_title_text(self, locator: str = 'ToolbarLocators.TITLE', - filter_text: str = None, timeout: int = 5000) -> str: + def get_toolbar_title_text(self, locator: str = ToolbarLocators.TITLE, + filter_text: str | None = None, + timeout: int = 5000) -> str: """Получает заголовок тулбара окна. Args: - locator: Локатор для заголовка тулбара (по умолчанию 'ToolbarLocators.TITLE') + locator: Локатор для заголовка тулбара filter_text: Текст для фильтрации заголовка (опционально) timeout: Таймаут ожидания в миллисекундах @@ -122,7 +124,7 @@ class ToolbarComponent(BaseComponent): # Получаем текст заголовка title_text = title_locator.text_content().strip() - logger.info("Заголовок тулбара: '%s'", title_text) + logger.info("Toolbar title: '%s'", title_text) return title_text @@ -167,32 +169,35 @@ class ToolbarComponent(BaseComponent): Args: message (str): Сообщение об ошибке если тулбар не виден """ - - locator = self.get_locator(ToolbarLocators.TITLE).filter(has_text=self.title) + locator = self.get_locator(ToolbarLocators.TITLE).filter( + has_text=self.title + ) expect(locator).to_be_visible(), message - def check_toolbar_presence_by_locator(self, locator: str|Locator, message: str) -> None: + def check_toolbar_presence_by_locator(self, locator: str | Locator, + message: str) -> None: """Проверяет видимость тулбара. Args: + locator: Локатор тулбара message (str): Сообщение об ошибке если тулбар не виден """ - locator = self.get_locator(locator) expect(locator).to_be_visible(), message - def check_toolbar_presence_by_locator_and_title(self, locator: str|Locator, message: str) -> None: + def check_toolbar_presence_by_locator_and_title(self, locator: str | Locator, + message: str) -> None: """Проверяет видимость тулбара. Args: + locator: Локатор тулбара message (str): Сообщение об ошибке если тулбар не виден """ - locator = self.get_locator(locator).filter(has_text=self.title) expect(locator).to_be_visible(), message def check_button_visibility(self, name: str) -> None: - """Проверяет наличие и видимость кнопки с предварительной прокруткой к элементу. + """Проверяет наличие и видимость кнопки с предварительной прокруткой. Args: name (str): Имя кнопки From 27ca4596fac2778621d8efc156913003af9fbb8b Mon Sep 17 00:00:00 2001 From: Radislav Date: Tue, 25 Nov 2025 13:59:33 +0300 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=20is=5Fitem=5Fvisible=20=D0=B2=20=D0=BD=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=B3=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D1=83=D1=8E=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен метод для проверки видимости элемента без исключений - Возвращает boolean значение для условных проверок --- components/navbar_component.py | 108 +++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/components/navbar_component.py b/components/navbar_component.py index 6825859..4a8b318 100644 --- a/components/navbar_component.py +++ b/components/navbar_component.py @@ -21,20 +21,6 @@ class NavigationPanelComponent(BaseComponent): super().__init__(page) # Действия: - def get_item_names(self, locator: str | Locator) -> list[str]: - """Возвращает тексты всех элементов по указанному локатору. - - Args: - locator: Локатор элементов или строка с CSS/XPath. - - Returns: - Список текстов элементов. - """ - - loc = self.get_locator(locator) - return loc.all_inner_texts() - - def click_item(self, locator: str | Locator, item_name: str) -> None: """Кликает по элементу с указанным текстом. @@ -50,13 +36,12 @@ class NavigationPanelComponent(BaseComponent): """Кликает по вложенному элементу с указанным текстом. Args: - node_root_locator: Локатор для поиска корневых элементов дерева (Локатор элемента или строка с CSS/XPath). + node_root_locator: Локатор для поиска корневых элементов дерева. item_name: Текст элемента для клика. """ def find_and_click_item(page, root_locator, item_name: str, parent: None|str) -> Locator|None: # Находим все локаторы корневых узлов на текущем уровне - #root_node = root_locator.locator('>div.v-treeview-node') nodes_count = root_locator.locator('>div.v-treeview-node').count() # Если искомый элемент находится на данном уровне, вычисляем локатор и делаем клик @@ -69,9 +54,9 @@ class NavigationPanelComponent(BaseComponent): if item_name == node_text: node_attr = node.get_attribute('class') if "v-treeview-node--leaf" not in node_attr: - toggle_button = node.\ - locator(NavigationPanelLocators.NODE_ROOT). \ - locator(NavigationPanelLocators.TOGGLE_BUTTON).first + toggle_button = node.locator( + NavigationPanelLocators.NODE_ROOT + ).locator(NavigationPanelLocators.TOGGLE_BUTTON).first toogle_class_attr = toggle_button.get_attribute('class') if "v-treeview-node__toggle--open" not in toogle_class_attr: toggle_button.click() @@ -93,17 +78,17 @@ class NavigationPanelComponent(BaseComponent): # Проверяем лист это или начало поддерева if "v-treeview-node--leaf" not in node_class_attr: # Проверяем, является ли узел раскрытым - class_attr = node.\ - locator(NavigationPanelLocators.NODE_ROOT). \ - locator(NavigationPanelLocators.TOGGLE_BUTTON).first.get_attribute('class') + class_attr = node.locator( + NavigationPanelLocators.NODE_ROOT + ).locator(NavigationPanelLocators.TOGGLE_BUTTON).first.get_attribute('class') if "v-treeview-node__toggle--open" in class_attr: is_expanded = True # Если узел закрыт можем его раскрыть if is_expanded is False: - toggle_button = node.\ - locator(NavigationPanelLocators.NODE_ROOT). \ - locator(NavigationPanelLocators.TOGGLE_BUTTON).first + toggle_button = node.locator( + NavigationPanelLocators.NODE_ROOT + ).locator(NavigationPanelLocators.TOGGLE_BUTTON).first toggle_button.click() # Ждем, пока дочерние элементы прогрузятся/появятся page.wait_for_timeout(1000) @@ -118,21 +103,27 @@ class NavigationPanelComponent(BaseComponent): # Рекурсивный вызов для дочерних элементов # Ищем дочерние элементы *внутри* текущего узла if has_children and is_expanded: - child_nodes_locator = root_locator.locator(f">div:nth-child({index + 1})").locator('>div.v-treeview-node__children') - found_loc = find_and_click_item(page, child_nodes_locator, item_name, parent=None) + child_nodes_locator = root_locator.locator( + f">div:nth-child({index + 1})" + ).locator('>div.v-treeview-node__children') + found_loc = find_and_click_item( + page, child_nodes_locator, item_name, parent=None + ) if found_loc: if parent is None: return found_loc else: - root_texts = root_locator.locator(f">div:nth-child({index + 1})").inner_text().splitlines() + root_texts = root_locator.locator( + f">div:nth-child({index + 1})" + ).inner_text().splitlines() if parent in root_texts: return found_loc # закрываем узел, если в нем ничего не нашли if is_expanded: - toggle_button = node.\ - locator(NavigationPanelLocators.NODE_ROOT). \ - locator(NavigationPanelLocators.TOGGLE_BUTTON).first + toggle_button = node.locator( + NavigationPanelLocators.NODE_ROOT + ).locator(NavigationPanelLocators.TOGGLE_BUTTON).first toggle_button.click() page.wait_for_timeout(1000) @@ -142,17 +133,33 @@ class NavigationPanelComponent(BaseComponent): root_locator = self.get_locator(node_root_locator) if parent: parent_loc = find_and_click_item(self.page, root_locator, parent, parent=None) - found = find_and_click_item(self.page, parent_loc.locator('>div.v-treeview-node__children'), item_name, parent=None) + found = find_and_click_item( + self.page, parent_loc.locator('>div.v-treeview-node__children'), + item_name, parent=None + ) else: found = find_and_click_item(self.page, root_locator, item_name, parent=None) assert found, f"Navigation panel item {item_name} is missing" - def traverse_panel_tree(self, node_root_locator: str | Locator, level=0, debug=False): - """ - Рекурсивно обходит дерево v-treeview и выводит информацию об элементах в режиме отладки (debug=True). + def get_item_names(self, locator: str | Locator) -> list[str]: + """Возвращает тексты всех элементов по указанному локатору. Args: - node_root_locator: Локатор для поиска корневых элементов дерева (Локатор элемента или строка с CSS/XPath). + locator: Локатор элементов или строка с CSS/XPath. + + Returns: + Список текстов элементов. + """ + + loc = self.get_locator(locator) + return loc.all_inner_texts() + + def traverse_panel_tree(self, node_root_locator: str | Locator, level=0, debug=False): + """ + Рекурсивно обходит дерево v-treeview и выводит информацию об элементах. + + Args: + node_root_locator: Локатор для поиска корневых элементов дерева. """ def traverse_tree(page, root_locator, level=0, debug=False): # Находим все локаторы корневых узлов на текущем уровне @@ -171,7 +178,8 @@ class NavigationPanelComponent(BaseComponent): # Проверяем лист это или начало поддерева if "v-treeview-node--leaf" in node_class_attr: if debug: - print(f'[{level}][{index}] {node_text} (LEAF, Expanded: {is_expanded}, Has Children: {has_children})') + leaf_msg = f'[{level}][{index}] {node_text} (LEAF, Expanded: {is_expanded}' + print(f"{leaf_msg}, Has Children: {has_children})") print("-----------------------------------------") else: # Проверяем, является ли узел раскрытым @@ -198,13 +206,16 @@ class NavigationPanelComponent(BaseComponent): if debug: # Выводим информацию об узле - print(f'[{level}][{index}] {edited_node_text} (NODE, Expanded: {is_expanded}, Has Children: {has_children})') + node_msg = f'[{level}][{index}] {edited_node_text} (NODE, Expanded: {is_expanded}' + print(f"{node_msg}, Has Children: {has_children})") print("-----------------------------------------") # Рекурсивный вызов для дочерних элементов # Ищем дочерние элементы *внутри* текущего узла if has_children and is_expanded: - child_nodes_locator = root_locator.locator(f">div:nth-child({index + 1})").locator('>div.v-treeview-node__children') + child_nodes_locator = root_locator.locator( + f">div:nth-child({index + 1})" + ).locator('>div.v-treeview-node__children') traverse_tree(page, child_nodes_locator, level+1, debug) root_locator = self.get_locator(node_root_locator) @@ -235,3 +246,22 @@ class NavigationPanelComponent(BaseComponent): else: loc = loc.get_by_text(item_name) self.check_visibility(loc, msg) + + def is_item_visible(self, locator: str | Locator, item_name: str) -> bool: + """ + Проверяет видимость элемента с указанным текстом без выбрасывания исключения. + + Args: + locator: Локатор элемента или строка с CSS/XPath. + item_name: Текст элемента для проверки. + + Returns: + bool: True если элемент видим, False если нет. + """ + element_locator = self.page.locator(locator).filter(has_text=item_name) + + # Сначала проверяем что элемент вообще существует + if element_locator.count() == 0: + return False + + return element_locator.is_visible() From a2d37a8090f68c5dc2e35e43d2af84c79857108e Mon Sep 17 00:00:00 2001 From: Radislav Date: Tue, 25 Nov 2025 14:35:04 +0300 Subject: [PATCH 7/9] =?UTF-8?q?refactor(users):=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=BF=D0=B5?= =?UTF-8?q?=D1=87=D0=B0=D1=82=D0=BA=D0=B8=20=D0=B8=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B0=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=BE=D0=B4=D0=B0=20>>=20>>=20-=20=D0=98=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=BF?= =?UTF-8?q?=D0=B5=D1=87=D0=B0=D1=82=D0=BA=D0=B0=20=D0=B2=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=BC=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0=20check=5Fnavigation=5Fpanel=5Fverticall=5Fscrolli?= =?UTF-8?q?ng=20=D0=BD=D0=B0=20check=5Fnavigation=5Fpanel=5Fvertical=5Fscr?= =?UTF-8?q?olling=20>>=20-=20=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=B4=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=B9=20?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?>>=20-=20=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BB?= =?UTF-8?q?=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B1=D0=B5?= =?UTF-8?q?=D0=BB=D1=8B=20=D0=B8=20=D0=BF=D1=80=D0=B8=D0=B2=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BA=20=D0=B5=D0=B4=D0=B8=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC=D1=83=20=D1=81=D1=82=D0=B8=D0=BB=D1=8E=20=D0=BE=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/users_tab.py | 306 ++++++++++++++++++++++----------------------- 1 file changed, 153 insertions(+), 153 deletions(-) diff --git a/pages/users_tab.py b/pages/users_tab.py index 9dc4a17..76c2d6f 100644 --- a/pages/users_tab.py +++ b/pages/users_tab.py @@ -50,6 +50,7 @@ class UsersTab(BasePage): self.alert = AlertComponent(page) # Действия: + def add_modal_window(self, window_type: str, title: str) -> None: """Добавляет модальное окно в коллекцию. @@ -70,98 +71,6 @@ class UsersTab(BasePage): else: assert False, "Unsupported modal window type" - def get_modal_window(self, title: str) -> ModalWindowComponent: - """Возвращает модальное окно по заголовку. - - Args: - title: Заголовок окна. - - Returns: - ModalWindowComponent: Экземпляр модального окна. - - Raises: - AssertionError: Если окно не найдено. - """ - - modal_window = self.modal_windows.get(title) - if modal_window is None: - assert False, f"Modal window with title '{title}' not found" - return modal_window - - def delete_modal_window(self, title: str) -> None: - """Удаляет модальное окно из коллекции. - - Args: - title: Заголовок окна. - - Raises: - AssertionError: Если окно не найдено. - """ - - if self.modal_windows.get(title) is None: - assert False, f"Modal window with title '{title}' not found" - self.modal_windows[title] = None - - def close_modal_window_by_toolbar_button(self, title: str) -> None: - """Закрывает модальное окно через кнопку в тулбаре. - - Args: - title: Заголовок окна. - """ - - modal_window = self.get_modal_window(title) - modal_window.close_window_by_toolbar_button() - self.delete_modal_window(title) - - def close_modal_window(self, title: str) -> None: - """Закрывает модальное окно через кнопку закрытия. - - Args: - title: Заголовок окна. - """ - - modal_window = self.get_modal_window(title) - modal_window.close_window() - self.delete_modal_window(title) - - def close_add_AD_user_window_by_toolbar_button(self) -> None: - """Закрывает окно добавления пользователя через тулбар.""" - - self.close_modal_window_by_toolbar_button("add_AD_user") - - def close_add_AD_user_window(self) -> None: - """Закрывает окно добавления пользователя.""" - - self.close_modal_window("add_AD_user") - - def close_add_user_window_by_toolbar_button(self) -> None: - """Закрывает окно добавления пользователя через тулбар.""" - - self.close_modal_window_by_toolbar_button("add_local_user") - - def close_add_user_window(self) -> None: - """Закрывает окно добавления пользователя.""" - - self.close_modal_window("add_local_user") - - def close_edit_user_window_by_toolbar_button(self, title: str) -> None: - """Закрывает окно редактирования через кнопку в тулбаре. - - Args: - title: Имя пользователя (заголовок окна). - """ - - self.close_modal_window_by_toolbar_button(title) - - def close_edit_user_window(self, title: str) -> None: - """Закрывает окно редактирования пользователя. - - Args: - title: Имя пользователя (заголовок окна). - """ - - self.close_modal_window(title) - def add_new_user(self, user_data: dict) -> bool: """Добавляет нового пользователя или обрабатывает ошибку при дубликате. @@ -193,7 +102,6 @@ class UsersTab(BasePage): self.alert.check_alert_absence(' Новый пользователь \n успешно добавлен! ') is_added = True elif alert_type == "error": - print(f' Имя {user_data["name"]} уже используется ') self.alert.check_alert_presence(f' Имя {user_data["name"]} уже \n используется ') self.alert.check_alert_absence(f' Имя {user_data["name"]} уже \n используется ') else: @@ -201,6 +109,80 @@ class UsersTab(BasePage): return is_added + def close_add_AD_user_window(self) -> None: + """Закрывает окно добавления пользователя.""" + + self.close_modal_window("add_AD_user") + + def close_add_AD_user_window_by_toolbar_button(self) -> None: + """Закрывает окно добавления пользователя через тулбар.""" + + self.close_modal_window_by_toolbar_button("add_AD_user") + + def close_add_user_window(self) -> None: + """Закрывает окно добавления пользователя.""" + + self.close_modal_window("add_local_user") + + def close_add_user_window_by_toolbar_button(self) -> None: + """Закрывает окно добавления пользователя через тулбар.""" + + self.close_modal_window_by_toolbar_button("add_local_user") + + def close_edit_user_window(self, title: str) -> None: + """Закрывает окно редактирования пользователя. + + Args: + title: Имя пользователя (заголовок окна). + """ + + self.close_modal_window(title) + + def close_edit_user_window_by_toolbar_button(self, title: str) -> None: + """Закрывает окно редактирования через кнопку в тулбаре. + + Args: + title: Имя пользователя (заголовок окна). + """ + + self.close_modal_window_by_toolbar_button(title) + + def close_modal_window(self, title: str) -> None: + """Закрывает модальное окно через кнопку закрытия. + + Args: + title: Заголовок окна. + """ + + modal_window = self.get_modal_window(title) + modal_window.close_window() + self.delete_modal_window(title) + + def close_modal_window_by_toolbar_button(self, title: str) -> None: + """Закрывает модальное окно через кнопку в тулбаре. + + Args: + title: Заголовок окна. + """ + + modal_window = self.get_modal_window(title) + modal_window.close_window_by_toolbar_button() + self.delete_modal_window(title) + + def delete_modal_window(self, title: str) -> None: + """Удаляет модальное окно из коллекции. + + Args: + title: Заголовок окна. + + Raises: + AssertionError: Если окно не найдено. + """ + + if self.modal_windows.get(title) is None: + assert False, f"Modal window with title '{title}' not found" + self.modal_windows[title] = None + def delete_user(self, user_name: str) -> None: """Удаляет пользователя. @@ -238,26 +220,6 @@ class UsersTab(BasePage): self.alert.check_alert_presence('\nОбновление успешно\n') self.alert.check_alert_absence('\nОбновление успешно\n') - def reset_password(self, user_name: str) -> str: - """Сбрасывает пароль пользователя. - - Args: - user_name: Имя пользователя. - - Returns: - str: Новый пароль (если получен). - """ - - new_password = "" - self.get_modal_window(user_name).reset_password() - - self.alert.check_alert_presence("") - alert_message = self.alert.get_text() - if len(alert_message) > 0: - new_password = re.findall(r'[\d]+', alert_message)[0] - - return new_password - def find_user_in_table(self, name: str, role: str) -> int: """Ищет пользователя в таблице. @@ -278,13 +240,32 @@ class UsersTab(BasePage): if len(table_content) == 0: assert False, "The contents of the table are missing" - del table_content[0] # Удаляем заголовок + # Удаляем заголовок + del table_content[0] for row_index, user_info in enumerate(table_content): if name in user_info and role in user_info: return row_index return -1 + def get_modal_window(self, title: str) -> ModalWindowComponent: + """Возвращает модальное окно по заголовку. + + Args: + title: Заголовок окна. + + Returns: + ModalWindowComponent: Экземпляр модального окна. + + Raises: + AssertionError: Если окно не найдено. + """ + + modal_window = self.modal_windows.get(title) + if modal_window is None: + assert False, f"Modal window with title '{title}' not found" + return modal_window + def open_add_user_window(self) -> None: """Открывает окно добавления пользователя. @@ -299,7 +280,7 @@ class UsersTab(BasePage): self.toolbar.check_button_visibility("add_user") self.toolbar.click_button("add_user") self.page.wait_for_timeout(700) - + self.add_modal_window("add_local_user", "") self.get_modal_window("add_local_user").check_by_window_title() @@ -325,7 +306,8 @@ class UsersTab(BasePage): if len(table_content) == 0: assert False, "The contents of the table are missing" - del table_content[0] # Удаляем заголовок + # Удаляем заголовок + del table_content[0] if row_index >= len(table_content): assert False, "Row_index is out of range" @@ -359,11 +341,30 @@ class UsersTab(BasePage): if row_index == -1: assert False, f"User with name {user_name} and role {role} has not been found" - self.page.locator(TableLocators.TABLE_WORK_AREA).locator( - "//tbody/tr").nth(row_index).click() + self.page.locator(TableLocators.TABLE_WORK_AREA).locator("//tbody/tr").nth(row_index).click() self.add_modal_window("edit_user", user_name) self.get_modal_window(user_name).check_by_window_title() + def reset_password(self, user_name: str) -> str: + """Сбрасывает пароль пользователя. + + Args: + user_name: Имя пользователя. + + Returns: + str: Новый пароль (если получен). + """ + + new_password = "" + self.get_modal_window(user_name).reset_password() + + self.alert.check_alert_presence("") + alert_message = self.alert.get_text() + if len(alert_message) > 0: + new_password = re.findall(r'[\d]+', alert_message)[0] + + return new_password + def transform_to_add_AD_user_window(self): """Трансформирует модальное окно добавления локального пользователя в окно добавления пользователя Active Directory с помощью нажатия @@ -387,6 +388,27 @@ class UsersTab(BasePage): self.add_modal_window("add_local_user", "") # Проверки: + def check_add_AD_user_window_content(self) -> None: + """Проверяет содержимое окна добавления пользователя через Active Directory.""" + + self.get_modal_window("add_AD_user").check_content() + + def check_add_user_window_content(self) -> None: + """Проверяет содержимое окна добавления локального пользователя.""" + + self.get_modal_window("add_local_user").check_content() + + def check_edit_user_window_content(self, user_name: str, role: str) -> None: + """Проверяет содержимое окна редактирования. + + Args: + user_name: Имя пользователя. + role: Роль пользователя. + """ + + edit_user_window = self.get_modal_window(user_name) + edit_user_window.check_content(user_name, role) + def check_users_table_content(self, verify: bool = False) -> None: """Проверяет содержимое таблицы пользователей. @@ -418,27 +440,6 @@ class UsersTab(BasePage): if verify: self.verify_users_table_content(table_content) - def check_add_user_window_content(self) -> None: - """Проверяет содержимое окна добавления локального пользователя.""" - - self.get_modal_window("add_local_user").check_content() - - def check_add_AD_user_window_content(self) -> None: - """Проверяет содержимое окна добавления пользователя через Active Directory.""" - - self.get_modal_window("add_AD_user").check_content() - - def check_edit_user_window_content(self, user_name: str, role: str) -> None: - """Проверяет содержимое окна редактирования. - - Args: - user_name: Имя пользователя. - role: Роль пользователя. - """ - - edit_user_window = self.get_modal_window(user_name) - edit_user_window.check_content(user_name, role) - def should_be_toolbar(self) -> None: """Проверяет наличие тулбара. @@ -468,18 +469,6 @@ class UsersTab(BasePage): self.toolbar.get_button_by_name("close").click() self.toolbar.check_button_visibility("edit") - def should_be_users_table(self) -> None: - """Проверяет наличие таблицы пользователей. - - Raises: - AssertionError: Если таблица отсутствует. - """ - - self.users_table.check_visibility( - TableLocators.TABLE_WORK_AREA, - "Users table is missing" - ) - def should_be_user_in_table(self, name: str, role: str) -> None: """Проверяет наличие пользователя в таблице. @@ -495,6 +484,15 @@ class UsersTab(BasePage): if found == -1: assert False, f"User with name {name} and role {role} has not been found" + def should_be_users_table(self) -> None: + """Проверяет наличие таблицы пользователей. + + Raises: + AssertionError: Если таблица отсутствует. + """ + + self.users_table.check_visibility(TableLocators.TABLE_WORK_AREA,"Users table is missing") + def should_not_be_user_in_table(self, name: str, role: str) -> None: """Проверяет отсутствие пользователя в таблице. @@ -547,7 +545,8 @@ class UsersTab(BasePage): if item["role"] is not None: role = item["role"] - if role in roles_dict: # Убрали вызов .keys() + # Убрали вызов .keys() + if role in roles_dict: item["role"] = roles_dict[role] user_info.append(item["role"]) else: @@ -565,7 +564,8 @@ class UsersTab(BasePage): expected_users_list.append(user_info) - del users_table[0] # Удаляем заголовок + # Удаляем заголовок + del users_table[0] self.check_lists_equals( users_table, From ad7cb98bb3e599c384998124f418e27599237cf0 Mon Sep 17 00:00:00 2001 From: Radislav Date: Tue, 25 Nov 2025 14:59:44 +0300 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BA=D0=B8=20=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82=D0=B2?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=8D=D0=BB=D0=B5=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=20=D0=BD=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен метод check_navigation_item_exists с возвратом boolean - Реализована мягкая проверка элементов навигационной панели --- pages/main_page.py | 159 ++++++++++++++++++++++++++++----------------- 1 file changed, 99 insertions(+), 60 deletions(-) diff --git a/pages/main_page.py b/pages/main_page.py index 97587c2..773e561 100644 --- a/pages/main_page.py +++ b/pages/main_page.py @@ -8,8 +8,8 @@ from playwright.sync_api import Page from locators.navigation_panel_locators import NavigationPanelLocators from components_derived.container_system_log_events import SystemLogEventsContainer from components_derived.user_card import UserCard -from components.navbar_component import NavigationPanelComponent from components.eventbar_component import EventPanelComponent +from components.navbar_component import NavigationPanelComponent from pages.base_page import BasePage class MainPage(BasePage): @@ -35,24 +35,6 @@ class MainPage(BasePage): self.event_panel = EventPanelComponent(page) # Действия: - def click_main_navigation_panel_item(self, item_name: str) -> None: - """Кликает по элементу основной панели навигации. - - Args: - item_name: Название элемента для клика. - """ - - self.navigation_panel.click_item(NavigationPanelLocators.PANEL_MAIN, item_name) - - def click_subpanel_item(self, item_name: str, parent=None) -> None: - """Выполняет рекурсивный поиск по панели навигации заданного элемента и делает клик по нему.""" - - active_item_locator = self.page.locator(NavigationPanelLocators.PANEL_MAIN).locator(NavigationPanelLocators.ACTIVE_CONTAINER) - node_locator = active_item_locator.locator(NavigationPanelLocators.SUB_PANEL_MAIN).locator(NavigationPanelLocators.TREEVIEW).first - - # Рекурсивный поиск в дереве v-treeview заданного элемента и клик по нему - self.navigation_panel.click_sub_item(node_locator, item_name, parent) - def click_events_panel_expand_less_button(self) -> None: """Выполняет нажатие кнопки галочка вверх.""" @@ -68,6 +50,35 @@ class MainPage(BasePage): return self.event_panel.click_system_log_tab() + def click_main_navigation_panel_item(self, item_name: str) -> None: + """Кликает по элементу основной панели навигации. + + Args: + item_name: Название элемента для клика. + """ + + self.navigation_panel.click_item( + NavigationPanelLocators.PANEL_MAIN, + item_name + ) + + def click_subpanel_item(self, item_name: str, parent=None) -> None: + """Выполняет рекурсивный поиск по панели навигации + заданного элемента и делает клик по нему.""" + + active_item_locator = self.page.locator( + NavigationPanelLocators.PANEL_MAIN + ).locator(NavigationPanelLocators.ACTIVE_CONTAINER) + node_locator = active_item_locator.locator( + NavigationPanelLocators.SUB_PANEL_MAIN + ).locator(NavigationPanelLocators.TREEVIEW).first + + # Рекурсивный поиск в дереве v-treeview заданного элемента + # и клик по нему + self.navigation_panel.click_sub_item( + node_locator, item_name, parent + ) + def click_user_button(self) -> UserCard: """Выполняет нажатие кнопки пользователя.""" @@ -78,17 +89,31 @@ class MainPage(BasePage): self.event_panel.do_logout() - def expand_navigation_subpanel(self): - """Выполняет полное открытие активной главной навигационной подпанели.""" + def expand_navigation_subpanel(self) -> None: + """Выполняет полное открытие активной главной + навигационной подпанели.""" - active_item_locator = self.page.locator(NavigationPanelLocators.PANEL_MAIN).locator(NavigationPanelLocators.ACTIVE_CONTAINER) - node_locator = active_item_locator.locator(NavigationPanelLocators.SUB_PANEL_MAIN).locator(NavigationPanelLocators.TREEVIEW).first + active_item_locator = self.page.locator( + NavigationPanelLocators.PANEL_MAIN + ).locator(NavigationPanelLocators.ACTIVE_CONTAINER) + node_locator = active_item_locator.locator( + NavigationPanelLocators.SUB_PANEL_MAIN + ).locator(NavigationPanelLocators.TREEVIEW).first - # Рекурсивный обход дерева v-treeview выбранной подпанели и вывод информации об элементах в режиме отладки (debug=True) - self.navigation_panel.traverse_panel_tree(node_locator, debug=False) + # Рекурсивный обход дерева v-treeview выбранной подпанели + # и вывод информации об элементах в режиме отладки (debug=True) + self.navigation_panel.traverse_panel_tree( + node_locator, debug=False + ) - def get_event_counters_by_tooltips(self) -> {}: - """Возвращает набор текстов всплывающих подсказок кнопок счетчиков событий.""" + def get_event_counters_by_buttons(self) -> dict: + """Возвращает набор значений кнопок счетчиков событий.""" + + return self.event_panel.get_event_button_values() + + def get_event_counters_by_tooltips(self) -> dict: + """Возвращает набор текстов всплывающих подсказок + кнопок счетчиков событий.""" events = self.event_panel.get_event_tooltip_texts() @@ -100,44 +125,27 @@ class MainPage(BasePage): event_counters[event_name] = count return event_counters - def get_event_counters_by_buttons(self) -> {}: - """Возвращает набор значений кнопок счетчиков событий.""" - - return self.event_panel.get_event_button_values() - def get_events_panel_position(self) -> str: - """Возвращает текущее положение панели событий относительно страницы: "top", "center","bottom".""" + """Возвращает текущее положение панели событий + относительно страницы: "top", "center","bottom".""" return self.event_panel.get_panel_position() - def scroll_navigation_panel_up(self) -> None: - """Прокручивает панель навигации вверх.""" - - self.navigation_panel.scroll_up(NavigationPanelLocators.PANEL_SCROLL_CONTAINER) - def scroll_navigation_panel_down(self) -> None: """Прокручивает панель навигации вниз.""" - self.navigation_panel.scroll_down(NavigationPanelLocators.PANEL_SCROLL_CONTAINER) - - # Проверки: - def should_be_navigation_panel(self) -> None: - """Проверяет наличие панели навигации.""" - - self.navigation_panel.check_visibility( - NavigationPanelLocators.PANEL_MAIN, - "Navigation panel is missing" + self.navigation_panel.scroll_down( + NavigationPanelLocators.PANEL_SCROLL_CONTAINER ) - def should_be_event_panel(self) -> None: - """Проверяет наличие элементов панели событий.""" + def scroll_navigation_panel_up(self) -> None: + """Прокручивает панель навигации вверх.""" - ## to-do: кнопки галочки??? - self.event_panel.should_be_tab_buttons() - self.event_panel.should_be_event_buttons() - self.event_panel.should_be_search_button() - self.event_panel.should_be_user_button() + self.navigation_panel.scroll_up( + NavigationPanelLocators.PANEL_SCROLL_CONTAINER + ) + # Проверки: def check_expand_less_button(self) -> bool: """Проверяет наличие кнопки галочка вверх.""" @@ -148,15 +156,18 @@ class MainPage(BasePage): return self.event_panel.check_expand_more_button() - def check_navigation_panel_verticall_scrolling(self) -> bool: - """Проверяет возможность вертикальной прокрутки панели. + def check_navigation_item_exists(self, item_name: str) -> bool: + """Проверяет существование элемента в навигационной панели. + + Args: + item_name: Название элемента для проверки Returns: - bool: True если прокрутка возможна, иначе False. + bool: True если элемент существует, False если нет """ - - return self.navigation_panel.is_scrollable_vertically( - NavigationPanelLocators.PANEL_SCROLL_CONTAINER + return self.navigation_panel.is_item_visible( + NavigationPanelLocators.PANEL_MAIN, + item_name ) def check_navigation_panel_item_visibility(self, item_name: str) -> None: @@ -170,3 +181,31 @@ class MainPage(BasePage): NavigationPanelLocators.PANEL_MAIN, item_name ) + + def check_navigation_panel_verticall_scrolling(self) -> bool: + """Проверяет возможность вертикальной прокрутки панели. + + Returns: + bool: True если прокрутка возможна, иначе False. + """ + + return self.navigation_panel.is_scrollable_vertically( + NavigationPanelLocators.PANEL_SCROLL_CONTAINER + ) + + def should_be_event_panel(self) -> None: + """Проверяет наличие элементов панели событий.""" + + ## to-do: кнопки галочки??? + self.event_panel.should_be_tab_buttons() + self.event_panel.should_be_event_buttons() + self.event_panel.should_be_search_button() + self.event_panel.should_be_user_button() + + def should_be_navigation_panel(self) -> None: + """Проверяет наличие панели навигации.""" + + self.navigation_panel.check_visibility( + NavigationPanelLocators.PANEL_MAIN, + "Navigation panel is missing" + ) From ed1658678a892aa685c2520287c7c97da37a2f61 Mon Sep 17 00:00:00 2001 From: Radislav Date: Tue, 25 Nov 2025 15:19:37 +0300 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=84=D0=B0=D0=B9=D0=BB=20pyproject.toml?= =?UTF-8?q?=20=D1=81=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B5=D0=B9=20pylint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Настройка pylint для проекта 'еНОД.Мониторинг' тестов Playwright - Отключены специфичные правила: * W0106: Разрешены выражения для побочных эффектов в тестах * W0246: Разрешены имена переменных в стиле Playwright (page, locator) * W1203: Разрешены f-strings для логирования вместо % форматирования - Установлена максимальная длина строки 120 символов --- pyproject.toml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a3420e4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.pylint.MASTER] +extension-pkg-allow-list = [] + +[tool.pylint.MESSAGES_CONTROL] +disable = [ + # W0106: Expression "%s" is assigned to nothing (pointless-statement) + # Отключен, так как в тестах могут использоваться выражения для побочных эффектов + "W0106", + + # W0246: Invalid variable name (invalid-name) + # Отключен, чтобы разрешить имена переменных в стиле Playwright (page, locator и т.д.) + "W0246", + + # W1203: Use % formatting in logging functions and pass the % parameters as arguments (logging-fstring-interpolation) + # Отключен, так как в проекте используется f-strings для логирования + "W1203", +] + +[tool.pylint.FORMAT] +max-line-length = 120 \ No newline at end of file