Добавлен тест для проверки соответствия данных API

pull/1/head
Radislav 2025-09-22 16:45:26 +03:00
parent e74138841d
commit 16c558a3a5
3 changed files with 225 additions and 167 deletions

View File

@ -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}"

View File

@ -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:"
)

View File

@ -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)