Files
codex 9d788b0663
CI / test (pull_request) Successful in 10m18s
CI / Fingerprint diagnostics (production deployment temporarily disabled) (pull_request) Has been skipped
fix: handle council specialty changes without JavaScript
2026-10-12 01:48:34 +03:00

267 lines
17 KiB
Python
Raw Permalink 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.
"""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()