"""Run from the repository root: python tests/browser_admin.py. Requires Playwright and Chromium. Uses in-memory data and intercepted requests; never contacts the production service. Optional BROWSER_EXECUTABLE selects an already installed Chromium instead of the Playwright download. """ import os from pathlib import Path import sys from tempfile import mkdtemp from urllib.parse import urlsplit sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from fastapi.testclient import TestClient from playwright.sync_api import expect, sync_playwright from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool from app.config import Settings, get_settings from app.db import Base, get_db from app.main import app from app.models import Employee from app.security import SESSION_COOKIE, sign_session def main(): engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool) Base.metadata.create_all(engine) factory = sessionmaker(bind=engine) with factory() as db: db.add_all(Employee(profile_key=f"staff:{i}", canonical_url=f"https://www.hse.ru/staff/{i}", full_name=f"Сотрудник {i:02d}", current_data={"positions": ["Преподаватель"]}) for i in range(30)) db.commit() def database(): with factory() as db: yield db settings = Settings(_env_file=None, session_secret="browser-test-secret") app.dependency_overrides[get_db] = database app.dependency_overrides[get_settings] = lambda: settings client = TestClient(app) client.cookies.set(SESSION_COOKIE, sign_session(settings.admin_username, settings)) output = Path(mkdtemp(prefix="miem-admin-browser-")) pending = [] def serve(route): url = urlsplit(route.request.url) if url.path == "/api/crawl-runs/latest": pending.append(route) return response = client.get(url.path + ("?" + url.query if url.query else "")) route.fulfill(status=response.status_code, body=response.content, content_type=response.headers.get("content-type", "text/plain")) try: with sync_playwright() as playwright: browser = playwright.chromium.launch(executable_path=os.getenv("BROWSER_EXECUTABLE")) page = browser.new_page(viewport={"width": 1440, "height": 1000}) page.route("**/*", serve) errors = [] page.on("pageerror", lambda error: errors.append(str(error))) page.goto("http://miem.test/admin/directory?limit=25&offset=25") expect(page.get_by_role("status")).to_have_text("Показаны 26–30 из 30") assert page.locator("th:visible").count() == 5 assert page.locator(".directory__table-wrap").evaluate("e => e.scrollWidth <= e.clientWidth") page.get_by_label("ФИО или ссылка").fill("Сотрудник 00") page.get_by_role("button", name="Применить фильтры").click() expect(page.get_by_role("status")).to_have_text("Показаны 1–1 из 1") assert "offset=" not in page.url trigger = page.get_by_role("button", name="Настроить колонки") trigger.focus() page.keyboard.press("Enter") dialog = page.get_by_role("dialog", name="Отображаемые колонки") expect(dialog).to_be_visible() for _ in range(22): page.keyboard.press("Tab") # Native dialogs may let Tab reach browser chrome, never background controls. assert dialog.evaluate("e => document.activeElement === document.body || e.contains(document.activeElement)") page.keyboard.press("Escape") expect(trigger).to_be_focused() trigger.click() page.get_by_role("button", name="Контакты", exact=True).click() page.keyboard.press("Escape") page.reload() expect(page.locator('th[data-column="email"]')).to_be_visible() page.evaluate("localStorage.clear()") page.goto("http://miem.test/admin/directory") page.screenshot(path=str(output / "desktop.png"), full_page=True) page.set_viewport_size({"width": 390, "height": 844}) assert page.evaluate("document.documentElement.scrollWidth <= innerWidth") assert page.locator('th[data-column="full_name"]').evaluate("e => getComputedStyle(e).position") == "sticky" page.screenshot(path=str(output / "mobile.png"), full_page=True) trigger.click() expect(dialog).to_be_visible() assert dialog.evaluate("e => e.getBoundingClientRect().right <= innerWidth") page.screenshot(path=str(output / "mobile-dialog.png"), full_page=True) page.keyboard.press("Escape") page.get_by_label("ФИО или ссылка").fill("Нет совпадений") page.get_by_role("button", name="Применить фильтры").click() expect(page.get_by_text("Сотрудники не найдены", exact=True)).to_be_visible() expect(page.get_by_role("link", name="Сбросить фильтры", exact=True)).to_be_visible() page.goto("http://miem.test/admin") page.wait_for_function("document.querySelector('[data-progress-panel]').getAttribute('aria-busy') === 'true'") pending.pop(0).fulfill(status=503, body="unavailable") expect(page.locator("[data-progress-error]")).to_be_visible() expect(page.get_by_text("Показаны последние полученные данные.", exact=False)).to_be_visible() page.get_by_role("button", name="Повторить").click() page.wait_for_timeout(100) pending.pop(0).fulfill(json={"running": None, "latest": None}) expect(page.locator("[data-progress-error]")).to_be_hidden() expect(page.get_by_text("Запусков пока нет. Запустите парсинг.", exact=True)).to_be_visible() assert not errors, errors browser.close() finally: app.dependency_overrides.clear() client.close() engine.dispose() print(f"Browser checks passed; screenshots: {output}") if __name__ == "__main__": main()