From 16c558a3a5116c990e344759964d9b8c6e230ee3 Mon Sep 17 00:00:00 2001 From: Radislav Date: Mon, 22 Sep 2025 16:45:26 +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=20=D1=82=D0=B5=D1=81=D1=82=20=D0=B4=D0=BB=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D1=82=D0=B2=D0=B5=D1=82=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/json_container_component.py | 159 +++++++++++++------ pages/templates_tab.py | 206 +++++++++++++------------ tests/e2e/test_templates_tab.py | 27 ++-- 3 files changed, 225 insertions(+), 167 deletions(-) diff --git a/components/json_container_component.py b/components/json_container_component.py index c7421d6..dfa6a61 100644 --- a/components/json_container_component.py +++ b/components/json_container_component.py @@ -5,8 +5,9 @@ """ import json -import jsondiff +import re from typing import Any, Dict +import jsondiff from playwright.sync_api import Page from tools.logger import get_logger from components.base_component import BaseComponent @@ -18,19 +19,111 @@ class JsonContainerComponent(BaseComponent): """Компонент для работы с JSON-данными на странице. Предоставляет методы чтения и проверки JSON-данных в контейнерах. - """ - def __init__(self, page: Page): + def __init__(self, page: Page) -> None: """Инициализирует JSON-контейнер. Args: page: Экземпляр страницы Playwright. """ + self.page = page - super().__init__(page) + def format_json_string(self, json_string: str) -> str: + """Форматирует строку JSON для корректного парсинга. + + Args: + json_string: Сырая строка с JSON-данными. + + Returns: + str: Отформатированная строка JSON. + """ + lines = json_string.splitlines() + formatted_lines = [] + stack = [] # Стек для отслеживания вложенности + current_indent = 0 + + for line in lines: + line = line.strip() + if not line: + continue + + # Определяем тип текущей строки + if line in ['{', '[']: + formatted_lines.append(' ' * current_indent + line) + stack.append(line) + current_indent += 1 + elif line in ['}', ']']: + current_indent -= 1 + if stack and stack[-1] in ['{', '[']: + stack.pop() + formatted_lines.append(' ' * current_indent + line) + elif re.match(r'^\d+:\{', line): + formatted_lines.append(' ' * current_indent + '{') + stack.append('{') + current_indent += 1 + elif ':' in line: + key, value = line.split(':', 1) + key = key.strip() + value = value.strip() + + if not (key.startswith('"') and key.endswith('"')): + key = f'"{key}"' + + if value in ['{', '[']: + formatted_line = f'{key}: {value}' + formatted_lines.append(' ' * current_indent + formatted_line) + stack.append(value) + current_indent += 1 + elif value in ['}', ']']: + current_indent -= 1 + formatted_line = f'{key}: {value}' + formatted_lines.append(' ' * current_indent + formatted_line) + if stack: + stack.pop() + else: + if (value and not value.isdigit() and + not value.replace('.', '', 1).isdigit() and + value not in ['true', 'false', 'null'] and + not value.startswith('"') and not value.endswith('"') and + not value.startswith('{') and not value.startswith('[')): + value = f'"{value}"' + + formatted_line = f'{key}: {value}' + formatted_lines.append(' ' * current_indent + formatted_line) + else: + formatted_lines.append(' ' * current_indent + line) + + # Добавляем запятые где необходимо + result = [] + total_lines = len(formatted_lines) + + for i, current_line in enumerate(formatted_lines): + if i < total_lines - 1: + next_line = formatted_lines[i + 1] + in_array = any(bracket == '[' for bracket in stack) + + # Упрощенная проверка условий для запятой + no_comma_condition1 = current_line.endswith(('{', '[', ',')) + no_comma_condition2 = next_line.strip().endswith(('}', ']')) + no_comma_condition3 = next_line.strip().startswith(('}', ']')) + no_comma_condition4 = in_array and next_line.strip() == ']' + + should_add_comma = not (no_comma_condition1 or no_comma_condition2 or + no_comma_condition3 or no_comma_condition4) + + # Специальный случай для элементов массива + if (in_array and current_line.strip() == '}' and + next_line.strip() != ']' and not next_line.strip().startswith('}')): + should_add_comma = True + + if should_add_comma: + current_line += ',' + + result.append(current_line) + + return '\n'.join(result) - # Действия: def read_data(self, locator: Any) -> Dict: """Читает и форматирует JSON-данные из указанного локатора. @@ -43,54 +136,19 @@ class JsonContainerComponent(BaseComponent): Raises: json.JSONDecodeError: Если данные не могут быть преобразованы в JSON. """ - - def format_json_string(json_string): - """Форматирует строку JSON для корректного парсинга. - - Args: - json_string: Сырая строка с JSON-данными. - - Returns: - str: Отформатированная строка JSON. - """ - - substrings = json_string.splitlines() - formatted_string_list = [] - last_substring = substrings.pop() - - for substring in substrings: - if substring.find(':') == -1: - if substring == '{': - formatted_string_list.append(substring) - elif substring == '}': - s1 = formatted_string_list.pop() - formatted_string_list.append(s1.rstrip(',')) - formatted_string_list.append(substring + ',') - else: - formatted_string_list.append(substring + ',') - continue - - key, value = substring.split(':') - s = ':'.join(['"' + key + '" ', " " + value]) - - if value == '{': - formatted_string_list.append(s) - else: - formatted_string_list.append(s + ',') - - s2 = formatted_string_list.pop() - formatted_string_list.append(s2.rstrip(',')) - formatted_string_list.append(last_substring) - - return " ".join(formatted_string_list) - - # Чтение JSON-содержимого из рабочей области loc = self.get_locator(locator) json_string = loc.inner_text() - formatted_json_string = format_json_string(json_string) - return json.loads(formatted_json_string) + formatted_json_string = self.format_json_string(json_string) + + try: + data = json.loads(formatted_json_string) + except json.JSONDecodeError as e: + logger.error("JSON decode error: %s", e) + logger.error("Formatted JSON: %s", formatted_json_string) + assert False, f"Invalid json content. Error: {e}" + + return data - # Проверки: def check_json_equals(self, actual: Any, expected: Any, msg: str) -> None: """Сравнивает JSON-объекты на идентичность. @@ -102,6 +160,5 @@ class JsonContainerComponent(BaseComponent): Raises: AssertionError: Если объекты не идентичны. """ - diff = jsondiff.diff(expected, actual, syntax='symmetric') assert len(diff) == 0, f"{msg}. DIFF is {diff}" diff --git a/pages/templates_tab.py b/pages/templates_tab.py index d2d558c..c5591ae 100644 --- a/pages/templates_tab.py +++ b/pages/templates_tab.py @@ -4,14 +4,22 @@ Позволяет проверять состояние и взаимодействовать с элементами вкладки. """ +import json +from pathlib import Path from playwright.sync_api import Page +from tools.logger import get_logger from locators.table_locators import TableLocators from locators.modal_window_locators import ModalWindowLocators +from locators.json_container_locators import JsonContainerLocators from components_derived.modal_view_template import ViewTemplateModalWindow from components.toolbar_component import ToolbarComponent from components.table_component import TableComponent +from components.json_container_component import JsonContainerComponent +from components.alert_component import AlertComponent from pages.base_page import BasePage +logger = get_logger("TEMPLATES_TAB") + class TemplatesTab(BasePage): """Класс для работы с вкладкой 'Шаблоны'. @@ -33,6 +41,9 @@ class TemplatesTab(BasePage): self.templates_table = TableComponent(page) self.modal_windows = {} + self.json_container = JsonContainerComponent(page) + self.alert = AlertComponent(page) + def add_modal_window(self, title: str) -> None: """Добавляет модальное окно в коллекцию. @@ -75,11 +86,14 @@ class TemplatesTab(BasePage): assert False, f"Modal window with title '{title}' not found" self.modal_windows[title] = None - def open_template_modal(self, row_index: int = 0) -> None: + def open_template_modal(self, row_index: int = 0) -> str: """Открывает модальное окно шаблона по клику на строку таблицы. Args: row_index: Индекс строки для клика (по умолчанию 0 - первая строка). + + Returns: + str: Имя шаблона. """ row_locator = self.templates_table.get_row_locator( TableLocators.TABLE_WORK_AREA, @@ -89,7 +103,8 @@ class TemplatesTab(BasePage): # Получаем имя шаблона из выбранной строки table_content = self.templates_table.read(TableLocators.TABLE_WORK_AREA) - template_name = table_content[row_index + 1][0] # +1 потому что первая строка - заголовки + # +1 потому что первая строка - заголовки + template_name = table_content[row_index + 1][0] # Добавляем модальное окно в коллекцию после открытия self.add_modal_window(template_name) @@ -125,29 +140,6 @@ class TemplatesTab(BasePage): return rows_count - 1 - def get_first_template_name(self) -> str: - """Получает имя шаблона из первой строки таблицы. - - Returns: - str: Имя шаблона из первого столбца первой строки. - - Raises: - AssertionError: Если таблица пуста или имя не найдено. - """ - table_content = self.templates_table.read(TableLocators.TABLE_WORK_AREA) - - if len(table_content) < 2: # Заголовок + хотя бы одна строка - assert False, "Table is empty or missing data rows" - - # Первая строка с данными (индекс 1, так как индекс 0 - заголовки) - first_row = table_content[1] - - if len(first_row) == 0: - assert False, "First row is empty" - - template_name = first_row[0] # Первый столбец - имя шаблона - return template_name - def scroll_templates_table_up(self) -> None: """Прокручивает таблицу шаблонов вверх.""" @@ -160,11 +152,74 @@ class TemplatesTab(BasePage): def scroll_modal_up(self) -> None: """Прокручивает содержимое модального окна вверх.""" - self.templates_table.scroll_up(ModalWindowLocators.MODAL_WINDOW_SCROLL_CONTAINER) + self.templates_table.scroll_up( + ModalWindowLocators.MODAL_WINDOW_SCROLL_CONTAINER + ) def scroll_modal_down(self) -> None: """Прокручивает содержимое модального окна вниз.""" - self.templates_table.scroll_down(ModalWindowLocators.MODAL_WINDOW_SCROLL_CONTAINER) + self.templates_table.scroll_down( + ModalWindowLocators.MODAL_WINDOW_SCROLL_CONTAINER + ) + + def extract_specific_template(self, template_name: str, response_data: dict) -> dict: + """Извлекает структуру конкретного шаблона по template_name из данных API. + + Args: + template_name: Имя шаблона для извлечения. + response_data: Данные ответа от API. + + Returns: + dict: Структура конкретного шаблона. + + Raises: + AssertionError: Если шаблон с указанным именем не найден + или структура ответа некорректна. + """ + # Проверяем, что ответ является списком шаблонов + assert isinstance(response_data, list), "API response is not a list of templates" + + # Ищем шаблон с указанным именем + for template in response_data: + if template.get('id') == template_name: + logger.info("Found template: %s", template_name) + return template + + # Если шаблон не найден + available_templates = [t.get('id', 'Unknown') for t in response_data] + + # Генерируем понятное сообщение об ошибке + error_msg = ( + f"Template '{template_name}' not found. " + f"Available templates: {available_templates}" + ) + logger.error(error_msg) + assert False, error_msg + + def save_template_data_to_file(self, template_data: dict, filename: str = None) -> str: + """Инструмент отладки. Сохраняет данные шаблона в JSON файл в текущей директории. + + Args: + template_data: Данные шаблона для сохранения. + filename: Имя файла (если None, генерируется автоматически). + + Returns: + str: Путь к сохраненному файлу. + """ + # Генерируем имя файла если не указано + if filename is None: + template_name = template_data.get('id', 'unknown_template') + filename = f"{template_name}_data.json" + + # Сохраняем файл в текущей директории + file_path = Path(filename) + + # Сохраняем данные в файл с форматированием + with open(file_path, 'w', encoding='utf-8') as file: + json.dump(template_data, file, ensure_ascii=False, indent=2) + + logger.info("Template data saved to: %s", file_path) + return str(file_path) def check_templates_modal_content(self, template_name: str) -> None: """Проверяет наличие и корректность элементов модального окна шаблона. @@ -292,7 +347,9 @@ class TemplatesTab(BasePage): Raises: AssertionError: Если модальное окно все еще видно. """ - is_visible = self.page.locator(ModalWindowLocators.MODAL_WINDOW).is_visible(timeout=1000) + is_visible = self.page.locator( + ModalWindowLocators.MODAL_WINDOW + ).is_visible(timeout=1000) if is_visible: assert False, "Modal window should not be visible" @@ -306,87 +363,32 @@ class TemplatesTab(BasePage): ModalWindowLocators.MODAL_WINDOW_SCROLL_CONTAINER ) - # Разрабатывается ========================================================= - def get_template_data_from_api(self, template_name: str) -> dict: - """Получает JSON данные конкретного шаблона из API. - - Args: - template_name: Имя шаблона. - - Returns: - dict: JSON данные шаблона из API. - - Raises: - AssertionError: Если не удалось получить данные из API. - """ - - # Отправляем запрос к API для получения данных конкретного шаблона - response = self.send_get_api_request("e-cmdb/api/device/template") - response_data = self.get_response_body(response) - print(response_data) - - if response.status_code != 200: - assert False, f"API request failed with status {response.status_code}" - - # Проверяем, что ответ содержит данные нужного шаблона - if 'name' in response_data and response_data['name'] != template_name: - assert False, f"API returned data for wrong template: expected '{template_name}', got '{response_data['name']}'" - - return response_data - - def verify_modal_content_with_api(self, template_name: str) -> None: - """Проверяет соответствие данных модального окна данным из API. + def verify_json_container_content(self, template_name: str, save_to_file: bool = False) -> None: + """Проверяет соответствие данных контейнера данным из API. Args: template_name: Имя шаблона для проверки. - - Raises: - AssertionError: Если данные не соответствуют API. + save_to_file: Флаг для сохранения данных в файл. """ - # Получаем данные из модального окна - modal_window = self.get_modal_window(template_name) - modal_data = modal_window.get_modal_content_data() - # Получаем данные из API - api_data = self.get_template_data_from_api(template_name) + # Читаем данные из контейнера + actual_data = self.json_container.read_data(JsonContainerLocators.CONTAINER) - # Сравниваем данные - self.compare_modal_with_api_data(modal_data, api_data, template_name) + # Отправляем запрос к backend для получения информации о шаблоне + response = self.send_get_api_request("e-cmdb/api/device/template") + response_body = self.get_response_body(response) + # Извлекаем конкретный шаблон по имени из ответа API + template_data = self.extract_specific_template(template_name, response_body) - def compare_modal_with_api_data(self, modal_data: dict, api_data: dict, template_name: str) -> None: - """Сравнивает JSON конфигурационные данные модального окна с данными из API. + # Сохраняем данные в файл если требуется + if save_to_file: + file_path = self.save_template_data_to_file(template_data) + logger.info("Template data saved to: %s", file_path) - Args: - modal_data: JSON данные из модального окна. - api_data: JSON данные из API. - template_name: Имя шаблона для сообщений об ошибках. - - Raises: - AssertionError: Если JSON конфигурационные данные не совпадают. - """ - # Проверяем, что modal_data содержит данные - if not modal_data: - assert False, f"No modal data found for template '{template_name}'" - - # Проверяем, что api_data содержит данные - if not api_data: - assert False, f"No API data found for template '{template_name}'" - - # Получаем конфигурационные данные из обоих источников - modal_config = modal_data.get('config', {}) - api_config = api_data.get('config', {}) - - # Проверяем, что оба источника содержат конфигурацию - if not modal_config: - assert False, f"No config data found in modal for template '{template_name}'" - - if not api_config: - assert False, f"No config data found in API for template '{template_name}'" - - # Сравниваем JSON конфигурации - self.check_equals( - modal_config, - api_config, - f"JSON config data mismatch for template '{template_name}'" + # Сравниваем actual_data с данными конкретного шаблона + self.json_container.check_json_equals( + actual_data, + template_data, + "Expected json content is not equal actual:" ) diff --git a/tests/e2e/test_templates_tab.py b/tests/e2e/test_templates_tab.py index 6140792..6ab7bf6 100644 --- a/tests/e2e/test_templates_tab.py +++ b/tests/e2e/test_templates_tab.py @@ -5,7 +5,6 @@ """ import pytest -from typing import Dict from playwright.sync_api import Page from pages.login_page import LoginPage from pages.main_page import MainPage @@ -18,13 +17,12 @@ class TestTemplatesTab: Проверяет корректность отображения и функциональность элементов вкладки Шаблоны. Тесты покрывают следующие сценарии: - 1. test_templates_tab_content - Проверка содержимого вкладки (тулбар, таблица шаблонов) - 2. test_templates_table_row_highlighting - Проверка выделения строк в таблице шаблонов - 3. test_templates_table_scrolling - Проверка вертикального скроллинга таблицы шаблонов - 4. test_templates_modal_window_content - Проверка содержимого модального окна шаблона - 5. test_templates_modal_window_scrolling - Проверка скроллинга модального окна шаблона - 6. test_templates_modal_window_api_data_consistency - [В разработке] - Проверка соответствия данных модального окна данным из API + 1. test_templates_tab_content - Проверка содержимого вкладки + 2. test_templates_table_row_highlighting - Проверка выделения строк в таблице + 3. test_templates_table_scrolling - Проверка вертикального скроллинга таблицы + 4. test_templates_modal_window_content - Проверка содержимого модального окна + 5. test_templates_modal_window_scrolling - Проверка скроллинга модального окна + 6. test_templates_modal_window_api_data_consistency - Проверка соответствия данных API """ @pytest.fixture(scope="function", autouse=True) @@ -129,12 +127,11 @@ class TestTemplatesTab: # Проверка видимости первой строки после прокрутки templates_tab.check_templates_table_first_row_visibility() else: - print("Таблица не поддерживает вертикальный скроллинг - проверяем базовую функциональность") + print("Таблица не поддерживает вертикальный скроллинг") # Проверка видимости первой строки templates_tab.check_templates_table_first_row_visibility() - #@pytest.mark.skip(reason="Временно исключено из тестирования") def test_templates_modal_window_content(self, browser: Page) -> None: """Тест содержимого модального окна шаблона. @@ -172,7 +169,6 @@ class TestTemplatesTab: # Проверяем, что модальное окно закрылось templates_tab.should_not_be_modal_window() - #@pytest.mark.skip(reason="Временно исключено из тестирования") def test_templates_modal_window_scrolling(self, browser: Page) -> None: """Тест скроллинга модального окна шаблона. @@ -222,7 +218,7 @@ class TestTemplatesTab: # Проверяем, что модальное окно закрылось templates_tab.should_not_be_modal_window() - @pytest.mark.skip(reason="Разрабатывается. Временно исключено из тестирования") + # @pytest.mark.skip(reason="Разрабатывается. Временно исключено из тестирования") def test_templates_modal_window_api_data_consistency(self, browser: Page) -> None: """Тест соответствия данных модального окна данным из API. @@ -238,7 +234,7 @@ class TestTemplatesTab: templates_tab.should_be_templates_table() # Добавляем задержку для загрузки данных - browser.wait_for_timeout(2000) + browser.wait_for_timeout(5000) # Открываем модальное окно, кликая на первую строку таблицы template_name = templates_tab.open_template_modal(0) @@ -250,7 +246,10 @@ class TestTemplatesTab: templates_tab.should_be_modal_window() # Проверка соответствия данных модального окна данным из API - templates_tab.verify_modal_content_with_api(template_name) + templates_tab.verify_json_container_content( + template_name, + save_to_file=False + ) # Закрытие модального окна через кнопку закрытия templates_tab.close_modal_window_by_toolbar_button(template_name)