Files
e2e_Pets/tests/conftest.py
VolandSZ cdca9d6003 feat: implement universal runner for standard and GOST TLS
- Add smart two-phase TLS detection with caching in tests/utils/tls.py

- Add auto-discovery for Chromium-GOST and Yandex.Browser

- Add --tls-mode and --ignore-https-errors CLI options

- Update browser_page context with ignore_https_errors and unified viewport

- Add unit tests in tests/utils/test_tls.py

- Update README.md and .env.example
2026-09-03 00:02:36 +03:00

180 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
# pyrefly: ignore [missing-import]
import pytest
# pyrefly: ignore [missing-import]
from dotenv import load_dotenv
# pyrefly: ignore [missing-import]
from playwright.sync_api import Page, Playwright
from tests.utils.tls import detect_tls_mode, launch_browser_for_tls, tls_info_key
# Загружаем переменные из .env
load_dotenv()
# Базовый URL портала — берётся из .env или дефолт
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
# Учётные данные для разных ролей — берутся из .env
CREDENTIALS = {
"vetdept": {
"login": os.getenv("VETDEPT_LOGIN"),
"password": os.getenv("VETDEPT_PASSWORD"),
},
"vetstation": {
"login": os.getenv("VETSTATION_LOGIN"),
"password": os.getenv("VETSTATION_PASSWORD"),
},
"prefecture": {
"login": os.getenv("PREFECTURE_LOGIN"),
"password": os.getenv("PREFECTURE_PASSWORD"),
},
}
def _get_role(request) -> str:
"""Определяет роль теста по pytest-маркеру."""
for marker in request.node.iter_markers():
if marker.name in CREDENTIALS:
return marker.name
pytest.fail("ОШИБКА: У теста нет маркера роли (например, @pytest.mark.vetdept)!")
def _login(page: Page, role: str) -> None:
"""Выполняет авторизацию на портале под указанной ролью."""
creds = CREDENTIALS[role]
if not creds["login"] or not creds["password"]:
pytest.fail(
f"ОШИБКА: Переменные окружения для роли '{role}' не заданы. "
f"Скопируй .env.example в .env и заполни данные."
)
print(f"\n[АВТОЛОГИН] Роль: {role} | URL: {BASE_URL}")
page.goto(f"{BASE_URL}/auth")
page.wait_for_load_state("networkidle")
page.get_by_role("textbox", name="Email").fill(creds["login"])
page.get_by_role("textbox", name="Введите пароль").fill(creds["password"])
page.get_by_role("button", name="Войти в аккаунт").click()
# Ожидаем успешного входа: переход с /auth в личный кабинет (/dashboard и т.д.)
page.wait_for_url(lambda url: "/auth" not in url, timeout=15000)
page.wait_for_load_state("networkidle")
# ---------------------------------------------------------------------------
# Фикстура: браузер с авто-адаптацией под тип TLS (session scope)
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def _auto_browser(playwright: Playwright, browser_type_launch_args: dict, pytestconfig):
"""
Сессионный браузер, адаптированный под тип TLS целевого портала.
Применяет аргументы запуска из pytest.ini и CLI (--headed, --slowmo, --tls-mode).
"""
# Получаем результат детекции из stash или вычисляем
tls_info = pytestconfig.stash.get(tls_info_key, None)
if tls_info is None:
cli_tls_mode = pytestconfig.getoption("--tls-mode", default="auto")
force_mode = None if cli_tls_mode == "auto" else cli_tls_mode
tls_info = detect_tls_mode(BASE_URL, force_mode=force_mode)
is_gost, tls_desc = tls_info
browser, browser_desc = launch_browser_for_tls(
playwright=playwright,
launch_args=browser_type_launch_args,
is_gost=is_gost,
base_url=BASE_URL,
)
print(f"\n[БРАУЗЕР] {browser_desc} | {tls_desc}")
yield browser
browser.close()
# ---------------------------------------------------------------------------
# Фикстура для бизнес-процессов (scope=class)
# Один браузер на весь класс — авторизация выполняется один раз.
# ---------------------------------------------------------------------------
@pytest.fixture(scope="class")
def browser_page(_auto_browser, request, pytestconfig):
"""
Авторизованная страница с жизненным циклом на весь класс (бизнес-процесс).
Все шаги внутри класса работают в одной вкладке браузера.
Браузер выбирается автоматически:
- Стандартный TLS -> встроенный Chromium
- ГОСТ TLS -> Chromium GOST (exe или CDP)
"""
role = _get_role(request)
ignore_https = pytestconfig.getoption("--ignore-https-errors", default=True)
context = _auto_browser.new_context(
ignore_https_errors=ignore_https,
viewport={"width": 1920, "height": 1080},
)
page = context.new_page()
_login(page, role)
yield page
context.close()
# ---------------------------------------------------------------------------
# Контейнер для передачи данных между шагами одного БП (scope=class)
# ---------------------------------------------------------------------------
@pytest.fixture(scope="class")
def bp_state() -> dict:
"""
Словарь для передачи данных между шагами одного бизнес-процесса.
Создаётся при старте класса, уничтожается после его завершения.
Пример:
def test_s01_create(self, browser_page, bp_state):
bp_state["claim_id"] = "12345"
def test_s02_verify(self, browser_page, bp_state):
assert bp_state["claim_id"] == "12345"
"""
return {}
# ---------------------------------------------------------------------------
# Устаревшая фикстура — оставлена для обратной совместимости
# ---------------------------------------------------------------------------
@pytest.fixture(scope="function")
def role_page(_auto_browser, request, pytestconfig):
"""
[DEPRECATED] Авторизованная страница с жизненным циклом на один тест.
Для новых тестов используй browser_page (scope=class).
"""
role = _get_role(request)
ignore_https = pytestconfig.getoption("--ignore-https-errors", default=True)
context = _auto_browser.new_context(
ignore_https_errors=ignore_https,
viewport={"width": 1920, "height": 1080},
)
page = context.new_page()
_login(page, role)
yield page
context.close()
# ---------------------------------------------------------------------------
# Хук для отображения читаемых имён шагов в pytest-html отчёте
# ---------------------------------------------------------------------------
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""
Заменяет технический путь к тесту в колонке Test отчёта pytest-html
на docstring метода (например, 'Шаг 1: Открыть главную страницу').
Если docstring отсутствует — оставляет оригинальный nodeid.
"""
outcome = yield
report = outcome.get_result()
# Берём docstring тестовой функции
doc = getattr(item.function, "__doc__", None)
if doc:
description = doc.strip().splitlines()[0] # только первая строка
report.nodeid = description