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