144 lines
7.5 KiB
Python
144 lines
7.5 KiB
Python
"""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")
|
||
skip_link = page.get_by_role("link", name="Перейти к содержимому")
|
||
assert skip_link.evaluate("e => getComputedStyle(e).left === '-999px'")
|
||
page.keyboard.press("Tab")
|
||
expect(skip_link).to_be_focused()
|
||
assert skip_link.evaluate("e => getComputedStyle(e).left === '8px'")
|
||
page.keyboard.press("Tab")
|
||
assert skip_link.evaluate("e => getComputedStyle(e).left === '-999px'")
|
||
|
||
for width in (1440, 1024, 921, 920, 768, 621, 620, 390, 320):
|
||
page.set_viewport_size({"width": width, "height": 844})
|
||
assert page.locator(".directory__filters").evaluate("e => e.scrollWidth <= e.clientWidth")
|
||
assert page.locator(".directory__input").evaluate_all("""controls => controls.every(control => {
|
||
const field = control.closest('.directory__field').getBoundingClientRect();
|
||
const rect = control.getBoundingClientRect();
|
||
return rect.left >= field.left - 1 && rect.right <= field.right + 1;
|
||
})""")
|
||
page.set_viewport_size({"width": 1440, "height": 1000})
|
||
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()
|