From a7c3e953f7f5d98fc755ad8049b72b8f7d4fac42 Mon Sep 17 00:00:00 2001 From: nsubbot Date: Fri, 15 Aug 2025 14:39:46 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=BA=D0=B8=20=D0=A1=D0=B5=D1=81=D1=81=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/base_component.py | 8 +- components/confirm_component.py | 16 +++ pages/base_page.py | 124 ++++++++++++++--------- pages/session_tab.py | 108 ++++++++++++++++++++ tests/e2e/test_sessions_tab.py | 168 +++++++++++++++++++++++++++----- 5 files changed, 346 insertions(+), 78 deletions(-) diff --git a/components/base_component.py b/components/base_component.py index 98e11bf..614f428 100644 --- a/components/base_component.py +++ b/components/base_component.py @@ -148,9 +148,7 @@ class BaseComponent: loc = self.get_locator(locator) - width = loc.evaluate("el => el.scrollWidth") - loc.scroll_into_view_if_needed() - self.page.mouse.wheel(-width, 0) + loc.evaluate("el => el.scrollBy(-el.scrollWidth, 0)") loc.wait_for(timeout=2000) @@ -170,9 +168,7 @@ class BaseComponent: loc = self.get_locator(locator) - width = loc.evaluate("el => el.scrollWidth") - loc.scroll_into_view_if_needed() - self.page.mouse.wheel(width, 0) + loc.evaluate("el => el.scrollBy(el.scrollWidth, 0)") loc.wait_for(timeout=2000) diff --git a/components/confirm_component.py b/components/confirm_component.py index 78bf60f..588f9fe 100644 --- a/components/confirm_component.py +++ b/components/confirm_component.py @@ -58,6 +58,16 @@ class ConfirmComponent(BaseComponent): """Нажимает кнопку закрытия окна подтверждения.""" self.close_button.click() + + def scroll_window_left(self) -> None: + """Прокручивает содержимое окна влево.""" + + self.scroll_left(ConfirmLocators.CONFIRM) + + def scroll_window_right(self) -> None: + """Прокручивает содержимое окна вправо.""" + + self.scroll_right(ConfirmLocators.CONFIRM) # Проверки: def check_title(self, title: str, msg: str) -> None: @@ -79,3 +89,9 @@ class ConfirmComponent(BaseComponent): """ self.text.check_have_text(text, msg) + + def check_window_horizontal_scrolling(self) -> bool: + """Проверяет возможность горизонтальной прокрутки окна.""" + + return self.is_scrollable_horizontally(ConfirmLocators.CONFIRM) + diff --git a/pages/base_page.py b/pages/base_page.py index 017b370..f25424f 100644 --- a/pages/base_page.py +++ b/pages/base_page.py @@ -1,14 +1,12 @@ -"""Модуль base_page содержит базовый класс для работы со страницами. +"""Базовый класс страницы для работы с Playwright. -Класс BasePage предоставляет общие методы для взаимодействия -со страницами через Playwright и выполнения API-запросов. +Содержит общие методы для взаимодействия со страницей и API. """ -import json -from typing import Any, Dict, List, Optional from playwright.sync_api import Page, Response, APIRequestContext, expect -from tools.logger import get_logger from data.environment import host +from tools.logger import get_logger +import json logger = get_logger("BASE_PAGE") @@ -16,54 +14,67 @@ logger = get_logger("BASE_PAGE") class BasePage: """Базовый класс для работы со страницами через Playwright. - Содержит общие методы для: - - Навигации по страницам - - Выполнения API-запросов - - Проверок состояния страницы + Атрибуты: + page (Page): Экземпляр страницы Playwright. """ - def __init__(self, page: Page) -> None: + def __init__(self, page: Page): """Инициализирует базовую страницу. Args: - page: Экземпляр страницы Playwright + page (Page): Экземпляр страницы Playwright. """ - self.page = page # Действия: def current_url(self) -> str: - """Возвращает текущий URL страницы.""" + """Возвращает текущий URL страницы. + Returns: + str: Текущий URL страницы. + """ return self.page.url - def open(self, uri: str) -> Optional[Response]: - """Открывает указанный URI на базовом URL.""" + def open(self, uri) -> Response | None: + """Открывает указанный URI в браузере. + Args: + uri (str): URI для открытия (без базового URL). + + Returns: + Response | None: Ответ сервера или None в случае ошибки. + """ return self.page.goto(f"{host.get_base_url()}{uri}", wait_until='domcontentloaded') def page_reload(self) -> None: """Перезагружает текущую страницу.""" - self.page.reload() - def wait_for_timeout(self, timeout: int) -> None: - """Ожидает указанное количество миллисекунд.""" + def wait_for_timeout(self, timeout): + """Ожидает указанное количество миллисекунд. + Args: + timeout (int): Время ожидания в миллисекундах. + """ self.page.wait_for_timeout(timeout) def get_api_request_context(self) -> APIRequestContext: - """Возвращает контекст для выполнения API-запросов.""" + """Возвращает контекст API-запросов. + Returns: + APIRequestContext: Контекст для выполнения API-запросов. + """ return self.page.context.request - def send_get_api_request(self, uri: str) -> Response: + def send_get_api_request(self, uri) -> Response: """Отправляет GET-запрос к API. Args: - uri: URI для запроса - """ + uri (str): URI API-эндпоинта (без базового URL). + Returns: + Response: Ответ сервера. + """ api_request_context = self.get_api_request_context() token = host.get_access_token() headers = {"Accept": "application/json", "Authorization": f"Bearer {token}"} @@ -73,14 +84,16 @@ class BasePage: ) return response - def send_post_api_request(self, uri: str, payload: Dict[str, Any]) -> Response: + def send_post_api_request(self, uri, payload) -> Response: """Отправляет POST-запрос к API. Args: - uri: URI для запроса - payload: Тело запроса - """ + uri (str): URI API-эндпоинта (без базового URL). + payload: Данные для отправки в теле запроса. + Returns: + Response: Ответ сервера. + """ api_request_context = self.get_api_request_context() token = host.get_access_token() headers = {"Accept": "application/json", "Authorization": f"Bearer {token}"} @@ -91,13 +104,15 @@ class BasePage: ) return response - def get_response_body(self, response: Response) -> Optional[Dict[str, Any]]: - """Возвращает тело ответа в формате JSON. + def get_response_body(self, response) -> dict | None: + """Извлекает тело ответа в формате JSON. Args: - response: Объект ответа - """ + response (Response): Ответ сервера. + Returns: + dict | None: Распарсенное тело ответа или None в случае ошибки. + """ try: response_body = response.json() except json.JSONDecodeError: @@ -107,39 +122,54 @@ class BasePage: # Проверки: def check_URL(self, uri: str, msg: str) -> None: - """Проверяет соответствие текущего URL ожидаемому. + """Проверяет, что текущий URL соответствует ожидаемому. Args: - uri: Ожидаемый URI - msg: Сообщение об ошибке - """ + uri (str): Ожидаемый URI (без базового URL). + msg (str): Сообщение об ошибке при несоответствии. + Raises: + AssertionError: Если URL не соответствует ожидаемому. + """ expect(self.page).to_have_url( f"{host.get_base_url()}{uri}", timeout=60000 ), msg - def check_equals(self, actual: Any, expected: Any, msg: str) -> None: + def check_equals(self, actual, expected, msg: str) -> None: """Проверяет равенство фактического и ожидаемого значений. Args: - actual: Фактическое значение - expected: Ожидаемое значение - msg: Сообщение об ошибке - """ + actual: Фактическое значение. + expected: Ожидаемое значение. + msg (str): Сообщение об ошибке при несоответствии. + Raises: + AssertionError: Если значения не равны. + """ assert actual == expected, msg - def check_lists_equals(self, actual: List[Any], expected: List[Any], msg: str) -> None: + def check_lists_equals(self, actual: list, expected: list, msg: str) -> None: """Рекурсивно проверяет равенство двух списков. Args: - actual: Фактический список - expected: Ожидаемый список - msg: Сообщение об ошибке - """ + actual (list): Фактический список. + expected (list): Ожидаемый список. + msg (str): Сообщение об ошибке при несоответствии. - def compare_lists(list1: List[Any], list2: List[Any]) -> bool: + Raises: + AssertionError: Если списки не равны. + """ + def compare_lists(list1: list, list2: list) -> bool: + """Вспомогательная функция для рекурсивного сравнения списков. + + Args: + list1 (list): Первый список для сравнения. + list2 (list): Второй список для сравнения. + + Returns: + bool: True если списки идентичны, иначе False. + """ if len(list1) != len(list2): return False for item1, item2 in zip(list1, list2): @@ -150,4 +180,4 @@ class BasePage: return False return True - assert compare_lists(actual, expected), msg + assert compare_lists(actual, expected), msg \ No newline at end of file diff --git a/pages/session_tab.py b/pages/session_tab.py index 85e7794..7100616 100644 --- a/pages/session_tab.py +++ b/pages/session_tab.py @@ -9,8 +9,10 @@ from locators.table_locators import TableLocators from locators.button_locators import ButtonLocators from elements.tooltip_button_element import TooltipButton from data.roles_dict import roles_dict +from data.environment import host from components.toolbar_component import ToolbarComponent from components.table_component import TableComponent +from components.confirm_component import ConfirmComponent from pages.base_page import BasePage class SessionsTab(BasePage): @@ -30,7 +32,10 @@ class SessionsTab(BasePage): self.toolbar = ToolbarComponent(page, "Сессия") self.sessions_table = TableComponent(page) + + self.delete_session_confirm = ConfirmComponent(page, " Отмена ", " Удалить ") + # Действия: def get_rows_count(self) -> int: """Возвращает количество строк в таблице (без заголовка). @@ -70,6 +75,42 @@ class SessionsTab(BasePage): button_locator = row_locator.locator(ButtonLocators.BUTTON_DELETE_SESSION) return TooltipButton(self.page, button_locator, "delete_session_button") + + def get_session_token(self) -> str: + """Возвращает токен текущего пользователя. + + Args: + + Returns: + str: Токен текущего пользователя + + Raises: + """ + return host.get_access_token() + + def find_session_in_table(self, token: str) -> int: + """Ищет сессию пользователя в таблице по выданному ему токену. + + Args: + token (str): Токен пользователя + + Returns: + int: Индекс строки или -1 если не найден + + Raises: + AssertionError: Если таблица пуста. + """ + table_content = self.sessions_table.read(TableLocators.TABLE_WORK_AREA) + if len(table_content) == 0: + assert False, "The contents of the table are missing" + + del table_content[0] # Удаляем заголовок + + for row_index, session_info in enumerate(table_content): + if token in session_info: + return row_index + return -1 + def scroll_sessions_table_up(self) -> None: """Прокручивает таблицу сессий вверх.""" @@ -80,6 +121,46 @@ class SessionsTab(BasePage): """Прокручивает таблицу сессий вниз.""" self.sessions_table.scroll_down(TableLocators.TABLE_SCROLL_CONTAINER) + + # Проверки: + def check_delete_session_confirm_window(self): + """ Проверяет контент и возможность горизонтального скроллинга окна подтверждения удаления сессии. """ + + # Поиск в таблице сессий сроки для текущего пользователя + session_token = self.get_session_token() + row_index = self.find_session_in_table(session_token) + if row_index == -1: + assert False, "Session for this token has not been found" + + # Найти кнопку удаления сессии и нажать на нее + delete_session_button = self.get_delete_session_button_from_row(row_index) + delete_session_button.click() + + # Проверка открытия окна подтверждения с заголовком "Удаление" + title = "Удаление" + self.delete_session_confirm.check_title( + title, + f"Confirmation dialog window text '{title}' is missing" + ) + + # Проверка текста в окне подтверждения + confirm_message = f"Удалить сессию {session_token}?" + self.delete_session_confirm.check_text( + confirm_message, + "Confirmation dialog window text does not match what is expected" + ) + + # Проверка горизонтального скроллинга + is_scrollable_horizontally = self.delete_session_confirm.check_window_horizontal_scrolling() + assert is_scrollable_horizontally, "Should be horizontal scrolling" + + self.delete_session_confirm.scroll_window_right() + self.page.wait_for_timeout(3000) + self.delete_session_confirm.scroll_window_left() + self.page.wait_for_timeout(2000) + + # Нажать кнопку "Отмена" + self.delete_session_confirm.click_cancel_button() def check_sessions_table_content(self, verify: bool = False) -> None: """Проверяет содержимое таблицы сессий. @@ -207,6 +288,33 @@ class SessionsTab(BasePage): f"Delete session button is missing on {row_index} row" ) delete_button.check_tooltip_with_text(ButtonLocators.TOOLTIP, tooltip) + + def should_be_session_in_table(self, token: str) -> None: + """Проверяет наличие сессии пользователя в таблице. + + Args: + token (str): Токен пользователя + + Raises: + AssertionError: Если сессия не найдена. + """ + found = self.find_session_in_table(token) + if found == -1: + assert False, "Session for this token has not been found" + + def should_not_be_session_in_table(self, token: str) -> None: + """Проверяет отсутствие сессии пользователя в таблице. + + Args: + token (str): Токен пользователя + + Raises: + AssertionError: Если сессия найдена. + """ + found = self.find_session_in_table(token) + if found != -1: + assert False, "Session for this token has been found" + def verify_sessions_table_content(self, sessions_table: list) -> None: """Сверяет данные таблицы с данными из БД. diff --git a/tests/e2e/test_sessions_tab.py b/tests/e2e/test_sessions_tab.py index 4f374f6..daff057 100644 --- a/tests/e2e/test_sessions_tab.py +++ b/tests/e2e/test_sessions_tab.py @@ -1,54 +1,172 @@ -"""Модуль тестов вкладки 'Сеансы'. - -Содержит тесты для проверки отображения и функциональности -элементов вкладки сеансов пользователей. -""" +from pages.login_page import LoginPage +from pages.main_page import MainPage +from pages.session_tab import SessionsTab +from pages.users_tab import UsersTab import pytest -from pages.session_tab import SessionsTab -from pages.main_page import MainPage -from pages.login_page import LoginPage -from playwright.sync_api import Page class TestSessionsTab: - """Класс тестов для проверки вкладки 'Сеансы'.""" + """Набор тестов для вкладки 'Сеансы'. + + Проверяет корректность отображения и функциональность элементов вкладки сеансов. + """ @pytest.fixture(scope="function", autouse=True) - def setup(self, browser: Page): - """Подготавливает тестовое окружение. + def setup(self, browser): + """Фикстура для подготовки тестового окружения. - Args: - browser: Экземпляр страницы Playwright. + Выполняет: + 1. Авторизацию в системе + 2. Переход на вкладку 'Сеансы' через панель навигации """ - # Авторизация в системе login_page = LoginPage(browser) login_page.do_login() - + # Инициализация главной страницы main_page = MainPage(browser) - + # Проверка и взаимодействие с элементами навигации main_page.should_be_navigation_panel() main_page.click_main_navigation_panel_item("Настройки") main_page.click_configuration_navigation_panel_item("Обслуживание и диагностика") main_page.click_maintenance_navigation_panel_item("Сеансы") + + def test_sessions_tab_content(self, browser): + """Тест содержимого вкладки 'Сеансы'. - - def test_sessions_tab_content(self, browser: Page): - """Проверяет содержимое вкладки 'Сеансы'. - - Args: - browser: Экземпляр страницы Playwright. + Проверяет: + 1. Наличие и корректность тулбара + 2. Наличие таблицы сеансов + 3. Соответствие содержимого таблицы данным из БД """ - # Инициализация страницы сеансов sessions_tab = SessionsTab(browser) # Проверка элементов интерфейса sessions_tab.should_be_toolbar() sessions_tab.should_be_sessions_table() - + # Проверка содержимого таблицы с верификацией данных из БД sessions_tab.check_sessions_table_content(verify=True) + + def test_sessions_table_row_highlighting(self, browser): + """Тест содержимого вкладки 'Сеансы'. + + Проверяет: + 1. Наличие таблицы сеансов + 2. Проверка подсветки строки при наведении на нее курсором + """ + # Инициализация страницы сеансов + sessions_tab = SessionsTab(browser) + + # Проверка элементов интерфейса + sessions_tab.should_be_sessions_table() + + # Получение количества строк в таблице без учета заголовка + rows_count = sessions_tab.get_rows_count() + + # Проверка подсветки первой строки + sessions_tab.check_sessions_table_row_highlighting(0) + + # Проверка подсветки последней строки строки (если в таблице более одной строки) + if rows_count > 1: + sessions_tab.check_sessions_table_row_highlighting(rows_count - 1) + + def test_delete_session_confirm_window(self, browser): + """Тест содержимого вкладки 'Сеансы'. + + Проверяет: + 1. Наличие таблицы сеансов + 2. Проверка контента и возможности горизонтального скроллинга окна подтверждения удаления сессии + """ + + # Инициализация страницы сеансов + sessions_tab = SessionsTab(browser) + + # Проверка элементов интерфейса + sessions_tab.should_be_sessions_table() + + # Проверка контента и скроллинга окна подтверждения удаления сессии + sessions_tab.check_delete_session_confirm_window() + + #@pytest.mark.develop + def test_session_for_new_user(self, browser): + """Тест содержимого вкладки 'Сеансы'. + + Проверяет: + 1. Создание нового пользователя + 2. Вход нового пользователя в систему + 3. Проверка наличия сессии нового пользователя + 4. Выход нового пользователя из системы (logout) + 5. Вход в систему первоначального пользователя + 6. Проверка отсутствия сессии нового пользователя + 7. Удаление нового пользователя + """ + user_data = {"name": "NewUser", "role": "Администратор", "password": "qwerty"} + + mp = MainPage(browser) + ut = UsersTab(browser) + + # Создание нового пользователя + mp.click_configuration_navigation_panel_item("Пользователи") + ut.open_add_user_window() + ut.add_new_user(user_data) + + # Обновление списка пользователей (двойной клик - возможно баг?) + mp.click_configuration_navigation_panel_item("Пользователи") + mp.click_configuration_navigation_panel_item("Пользователи") + + # Проверка наличия пользователя в таблице + ut.should_be_user_in_table(user_data["name"], user_data["role"]) + + # Вход в систему для нового пользователя + new_lp = LoginPage(browser) + new_lp.do_login(username=user_data["name"], password=user_data["password"]) + + # Инициализация главной страницы + new_mp = MainPage(browser) + + # Открыть вкладку Сессии + new_mp.should_be_navigation_panel() + new_mp.click_main_navigation_panel_item("Настройки") + new_mp.click_configuration_navigation_panel_item("Обслуживание и диагностика") + new_mp.click_maintenance_navigation_panel_item("Сеансы") + + # Инициализация страницы сеансов + st = SessionsTab(browser) + + # Проверка элементов интерфейса + st.should_be_sessions_table() + + # Проверка наличия записи о сессии текущего пользователя + session_token = st.get_session_token() + st.should_be_session_in_table(session_token) + + # logout + new_mp.do_logout() + + # Авторизация в системе предыдущего пользователя + prev_lp = LoginPage(browser) + prev_lp.do_login() + + # Инициализация главной страницы + prev_mp = MainPage(browser) + + # Открыть вкладку Сессии + prev_mp.should_be_navigation_panel() + prev_mp.click_main_navigation_panel_item("Настройки") + prev_mp.click_configuration_navigation_panel_item("Обслуживание и диагностика") + prev_mp.click_maintenance_navigation_panel_item("Сеансы") + + # Проверка элементов интерфейса + st.should_be_sessions_table() + + # Проверка отсутствия записи о сессии созданного пользователя после выхода из системы + st.should_not_be_session_in_table(session_token) + + # Удаление созданного пользователя + prev_ut = UsersTab(browser) + prev_ut.open_edit_user_page_by_user(user_data["name"], user_data["role"]) + prev_ut.delete_user(user_data["name"])