267 lines
17 KiB
Python
267 lines
17 KiB
Python
"""Offline public council UX check: python tests/browser_council.py.
|
||
|
||
Requires Playwright/Chromium; BROWSER_EXECUTABLE may select an installed browser.
|
||
Synthetic employees and intercepted requests; no production access.
|
||
"""
|
||
import os
|
||
from io import BytesIO
|
||
from pathlib import Path
|
||
import sys
|
||
from urllib.parse import urlsplit
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||
|
||
from fastapi.testclient import TestClient
|
||
from openpyxl import load_workbook
|
||
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.db import Base, get_db
|
||
from app.main import app
|
||
from app.models import Employee, EmployeePublication
|
||
from app.services import council
|
||
|
||
|
||
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:
|
||
for number in range(28):
|
||
person = Employee(profile_key=f"test:{number}", full_name=f"Сотрудник {number:02d}",
|
||
canonical_url="https://www.hse.ru/staff/test",
|
||
current_data={"sections": [{"title": "Учёные степени",
|
||
"items": ["Доктор технических наук"]}]})
|
||
db.add(person)
|
||
db.flush()
|
||
db.add(EmployeePublication(employee_id=person.id, title="Исследование методов передачи данных в фотонных интегральных схемах",
|
||
year=2024, publication_type="ARTICLE", publication_id=f"{number}",
|
||
raw_data={"journal": "Бизнес-информатика"} if number == 27 else
|
||
{"journal": "BIT Numerical Mathematics", "issn": "0006-3835"} if number == 25 else {},
|
||
url=f"https://publications.hse.ru/view/{number}", source_hash=f"test:{number}"))
|
||
if number == 27:
|
||
db.add(EmployeePublication(employee_id=person.id, title="Дополнительная статья", year=2024,
|
||
publication_type="ARTICLE", source_hash="test:extra"))
|
||
for name, degree in [("Без степени", None), ("Только звание", "Профессор")]:
|
||
person = Employee(profile_key=name, full_name=name, canonical_url="https://www.hse.ru/staff/test",
|
||
current_data={"sections": [{"title": "Учёные степени", "items": [degree] if degree else []}]})
|
||
db.add(person)
|
||
db.flush()
|
||
db.add(EmployeePublication(employee_id=person.id, title="Не включать в совет", year=2024, source_hash=name))
|
||
db.commit()
|
||
|
||
def database():
|
||
with factory() as db:
|
||
yield db
|
||
|
||
original = council._articles_by_author
|
||
council._articles_by_author = lambda: {"сотрудник 26": [
|
||
{"title": f"Публикация из данных ВШЭ {number}", "year": 2024, "doi": f"10.1234/test-{number}",
|
||
"types": [], "journals": [], "quartiles": ["Scopus Q1"], "origins": ["Данные ВШЭ"], "rows": [number + 2],
|
||
"in_csv": True, "in_db": False, "url": None} for number in range(2)]}
|
||
app.dependency_overrides[get_db] = database
|
||
client = TestClient(app)
|
||
output = Path(".impeccable/review")
|
||
output.mkdir(parents=True, exist_ok=True)
|
||
|
||
requests = {"articles": 0, "fail": False}
|
||
|
||
def serve(route):
|
||
url = urlsplit(route.request.url)
|
||
if '/council/employees/' in url.path:
|
||
requests['articles'] += 1
|
||
if requests['fail']:
|
||
route.fulfill(status=503, body='Temporary failure')
|
||
return
|
||
response = client.get(url.path + ("?" + url.query if url.query else ""))
|
||
route.fulfill(status=response.status_code, body=response.content,
|
||
headers={key: value for key, value in response.headers.items()
|
||
if key in {"content-type", "content-disposition"}})
|
||
|
||
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/council")
|
||
expect(page.locator(".council__list-heading [role=status]")).to_have_text("Показаны 1–25 из 28")
|
||
assert "Без степени" not in page.content() and "Только звание" not in page.content()
|
||
expect(page.locator("#council-date")).to_have_count(0)
|
||
assert "Монографии" not in page.content()
|
||
branches = page.locator('#council-branch')
|
||
specialty = page.locator('#council-specialty')
|
||
expect(branches.locator('option')).to_have_count(2)
|
||
specialty.select_option('1.1.2')
|
||
expect(branches.locator('option')).to_have_count(1)
|
||
expect(branches).to_have_value('физико-математические')
|
||
specialty.select_option('2.3.2')
|
||
expect(branches.locator('option')).to_have_count(1)
|
||
expect(branches).to_have_value('технические')
|
||
specialty.select_option('1.2.2')
|
||
expect(branches.locator('option')).to_have_count(2)
|
||
branches.select_option('физико-математические')
|
||
specialty.select_option('')
|
||
expect(branches).to_have_value('физико-математические')
|
||
branches.select_option('технические')
|
||
page.get_by_role("button", name="Применить", exact=True).click()
|
||
expect(page.get_by_role("link", name="Вход в систему")).to_be_visible()
|
||
page.get_by_role("link", name="Далее").click()
|
||
expect(page.locator(".council__list-heading [role=status]")).to_have_text("Показаны 26–28 из 28")
|
||
page.get_by_role("link", name="Назад").click()
|
||
summary = page.locator(".council__candidate-summary").first
|
||
assert requests['articles'] == 0
|
||
requests['fail'] = True
|
||
summary.focus()
|
||
page.keyboard.press("Enter")
|
||
expect(page.get_by_role('button', name='Повторить загрузку публикаций')).to_be_visible()
|
||
requests['fail'] = False
|
||
page.get_by_role('button', name='Повторить загрузку публикаций').click()
|
||
expect(page.locator(".council__source").filter(has_text="ЛК сотрудника ВШЭ").first).to_be_visible()
|
||
before = requests['articles']
|
||
summary.click()
|
||
summary.click()
|
||
assert requests['articles'] == before
|
||
warning = page.get_by_role('button', name='Уточнение: тип материала').first
|
||
summary.focus()
|
||
warning.hover()
|
||
expect(summary).to_be_focused()
|
||
expect(page.get_by_role('tooltip')).to_be_visible()
|
||
page.keyboard.press('Escape')
|
||
expect(page.get_by_role('tooltip')).to_have_count(0)
|
||
expect(warning).to_have_attribute('aria-expanded', 'false')
|
||
assert warning.evaluate("button => button.matches(':hover')")
|
||
expect(summary).to_be_focused()
|
||
page.mouse.move(5, 5)
|
||
warning.hover()
|
||
expect(page.get_by_role('tooltip')).to_be_visible()
|
||
expect(page.get_by_role('tooltip')).to_contain_text('тип материала')
|
||
page.screenshot(path=str(output / 'warning-desktop.png'))
|
||
page.set_viewport_size({'width': 320, 'height': 844})
|
||
warning.focus()
|
||
page.keyboard.press('Enter')
|
||
expect(page.get_by_role('tooltip')).to_be_visible()
|
||
assert page.evaluate('document.documentElement.scrollWidth <= innerWidth')
|
||
page.screenshot(path=str(output / 'warning-mobile.png'))
|
||
page.keyboard.press('Escape')
|
||
expect(page.get_by_role('tooltip')).to_have_count(0)
|
||
page.set_viewport_size({'width': 1440, 'height': 1000})
|
||
page.get_by_role("button", name="Уточнение: квартиль и основание зачёта").first.click()
|
||
expect(page.get_by_role("tooltip").first).to_contain_text("Нет подтверждённого основания")
|
||
page.keyboard.press("Escape")
|
||
assert page.locator(".council__candidate").first.evaluate("e => e.open")
|
||
page.screenshot(path=str(output / "desktop.png"), full_page=True)
|
||
page.screenshot(path=str(output / "sorting-desktop.png"), full_page=False)
|
||
for width in (1024, 768, 700, 390, 320):
|
||
page.set_viewport_size({"width": width, "height": 844})
|
||
assert page.evaluate("document.documentElement.scrollWidth <= innerWidth"), width
|
||
assert summary.evaluate("e => e.scrollWidth <= e.clientWidth"), width
|
||
page.set_viewport_size({"width": 390, "height": 844})
|
||
page.screenshot(path=str(output / "mobile.png"), full_page=True)
|
||
page.screenshot(path=str(output / "sorting-mobile.png"), full_page=False)
|
||
for sort, direction, first in [("full_name", "asc", "Сотрудник 00"),
|
||
("full_name", "desc", "Сотрудник 27"),
|
||
("db_count", "desc", "Сотрудник 27"),
|
||
("count", "desc", "Сотрудник 26"),
|
||
("total_count", "desc", "Сотрудник 26")]:
|
||
page.get_by_label("Сортировать по").select_option(sort)
|
||
page.get_by_label("Направление").select_option(direction)
|
||
page.get_by_role("button", name="Применить", exact=True).click()
|
||
expect(page.locator(".council__candidate-identity strong").first).to_have_text(first)
|
||
page.get_by_label("Сортировать по").select_option("full_name")
|
||
page.get_by_label("Направление").select_option("desc")
|
||
page.get_by_role("button", name="Применить", exact=True).click()
|
||
page.get_by_role("link", name="Далее").click()
|
||
expect(page.get_by_label("Сортировать по")).to_have_value("full_name")
|
||
expect(page.get_by_label("Направление")).to_have_value("desc")
|
||
expect(page.locator(".council__candidate-identity strong").first).to_have_text("Сотрудник 02")
|
||
with page.expect_download() as pending_download:
|
||
page.get_by_role("button", name="Скачать XLSX", exact=True).click()
|
||
download = pending_download.value
|
||
assert download.suggested_filename == "council.xlsx"
|
||
download.save_as(str(output / "council.xlsx"))
|
||
workbook = load_workbook(output / "council.xlsx")
|
||
assert workbook["Сотрудники"].max_row == 29
|
||
assert workbook["Сотрудники"]["B2"].value == "Сотрудник 27"
|
||
page.get_by_label("Поиск по ФИО").fill("Сотрудник 27")
|
||
page.get_by_role("button", name="Применить", exact=True).click()
|
||
expect(page.locator(".council__list-heading [role=status]")).to_have_text("Показаны 1–1 из 1 · Всего в списке: 28")
|
||
expect(page.get_by_label("Сортировать по")).to_have_value("full_name")
|
||
expect(page.get_by_label("Направление")).to_have_value("desc")
|
||
with page.expect_download() as pending_download:
|
||
page.get_by_role("button", name="Скачать XLSX", exact=True).click()
|
||
filtered = load_workbook(BytesIO(Path(pending_download.value.path()).read_bytes()))
|
||
assert filtered["Сотрудники"].max_row == 2
|
||
page.get_by_label("Поиск по ФИО").fill("Нет совпадений")
|
||
page.get_by_role("button", name="Применить", exact=True).click()
|
||
expect(page.get_by_text("Сотрудники не найдены", exact=True)).to_be_visible()
|
||
page.get_by_role("link", name="Сбросить поиск").first.click()
|
||
expect(page.locator(".council__list-heading [role=status]")).to_have_text("Показаны 1–25 из 28")
|
||
expect(page.get_by_label("Сортировать по")).to_have_value("full_name")
|
||
expect(page.get_by_label("Направление")).to_have_value("desc")
|
||
page.get_by_label("Специальность ВАК").select_option("1.2.2")
|
||
page.get_by_role("button", name="Применить", exact=True).click()
|
||
expect(page.locator(".council__list-heading [role=status]")).to_have_text("Показаны 1–1 из 1 · Всего в списке: 28")
|
||
expect(page.locator(".council__candidate-identity strong")).to_have_text("Сотрудник 27")
|
||
page.locator(".council__candidate-summary").click()
|
||
expect(page.get_by_text("Публикаций по выбранной специальности: 1", exact=True)).to_be_visible()
|
||
expect(page.get_by_text("Специальность: 1.2.2", exact=False).first).to_be_visible()
|
||
for width in (1440, 768, 390, 320):
|
||
page.set_viewport_size({"width": width, "height": 900})
|
||
assert page.evaluate("document.documentElement.scrollWidth <= innerWidth"), width
|
||
page.screenshot(path=str(output / f"specialty-{width}.png"), full_page=False)
|
||
with page.expect_download() as pending_download:
|
||
page.get_by_role("button", name="Скачать XLSX", exact=True).click()
|
||
filtered = load_workbook(BytesIO(Path(pending_download.value.path()).read_bytes()))
|
||
assert filtered["Сотрудники"].max_row == 2
|
||
assert dict(filtered["Параметры"].values)["Специальность ВАК"].startswith("1.2.2.")
|
||
headers = {cell.value: cell.column for cell in filtered["Сотрудники"][1]}
|
||
assert filtered["Сотрудники"].cell(2, headers["Специальности ВАК"]).value.startswith("1.2.2.")
|
||
assert "Работ по выбранной специальности" not in headers
|
||
pending_download.value.save_as(str(output / "specialty-export.xlsx"))
|
||
page.get_by_label("Специальность ВАК").select_option("")
|
||
page.get_by_label("Поиск по ФИО").fill("Сотрудник 25")
|
||
page.get_by_role("button", name="Применить", exact=True).click()
|
||
page.locator(".council__candidate-summary").click()
|
||
expect(page.get_by_text("BIT Numerical Mathematics", exact=False).first).to_be_visible()
|
||
for width in (1440, 390, 320):
|
||
page.set_viewport_size({"width": width, "height": 900})
|
||
assert page.evaluate("document.documentElement.scrollWidth <= innerWidth"), width
|
||
page.screenshot(path=str(output / f"catalog-{width}.png"), full_page=True)
|
||
assert not errors, errors
|
||
no_js = browser.new_context(java_script_enabled=False)
|
||
no_js.route("**/*", serve)
|
||
fallback = no_js.new_page()
|
||
for previous, specialty, old_branch, new_branch in [
|
||
("2.3.2", "1.1.2", "технические", "физико-математические"),
|
||
("1.1.2", "2.3.2", "физико-математические", "технические"),
|
||
]:
|
||
url = f"http://miem.test/admin/council?specialty={previous}&branch={old_branch}&q=Тест&sort=full_name&direction=asc"
|
||
fallback.goto(url)
|
||
fallback.get_by_label("Специальность ВАК").select_option(specialty)
|
||
with fallback.expect_download() as pending_download:
|
||
fallback.get_by_role("button", name="Скачать XLSX", exact=True).click()
|
||
book = load_workbook(BytesIO(Path(pending_download.value.path()).read_bytes()))
|
||
assert dict(book["Параметры"].values)["Отрасль оценки"] == new_branch
|
||
fallback.get_by_role("button", name="Применить", exact=True).click()
|
||
expect(fallback.get_by_label("Отрасль оценки")).to_have_value(new_branch)
|
||
expect(fallback.get_by_label("Специальность ВАК")).to_have_value(specialty)
|
||
expect(fallback.get_by_label("Поиск по ФИО")).to_have_value("Тест")
|
||
expect(fallback.get_by_label("Сортировать по")).to_have_value("full_name")
|
||
expect(fallback.get_by_label("Направление")).to_have_value("asc")
|
||
no_js.close()
|
||
browser.close()
|
||
finally:
|
||
council._articles_by_author = original
|
||
app.dependency_overrides.clear()
|
||
client.close()
|
||
engine.dispose()
|
||
print(f"Council browser checks passed; screenshots: {output}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|