fix(ui): пункты меню не прячутся под футером; сквозные сценарии панели
Some checks failed
ci / test (3.10) (push) Has been cancelled
ci / test (3.11) (push) Has been cancelled
ci / test (3.12) (push) Has been cancelled

web/index.html:
- мобильное меню после раскрытия не держит анимационную высоту Bootstrap:
  нижние пункты (Обзор/История/API-ключи) уезжали под .sidebar-footer и не нажимались
- на десктопе рельс #nav скроллится сам, футер не сжимается; активный пункт
  доскролливается в область видимости
- failure() принимает заголовок: ошибка валидации источника больше не подписана
  «Не удалось загрузить данные»

tools/reachability.py: общая проверка «элемент виден, но перекрыт другим» —
hit-test по центру с scrollIntoView, пропуск скрытых узлов и закрытых <details>.

tools/panel_scenarios.py: 11 сквозных сценариев реального пользователя —
deep-link фильтр, экспорт CSV/JSON по байтам, читалка и ссылка на оригинал,
дайджест, поиск источников, Telegram-сессия, форма источника (отказ без записи
в реестр), клавиатурный flow (/ ? Esc 1-9) со сверкой строк ленты с API,
мобильное меню, адаптивность 320-1920 без горизонтального переполнения, зум.

tools/ui_smoke.py: reachability-проверки десктопного рельса, контролов и мобильного меню.
tests: +3 регрессии на CSS/заголовки ошибок; pytest 93 passed, smoke exit 0,
сценарии exit 0, page_errors пуст.
This commit is contained in:
Vlad Gothbreach
2026-10-27 05:45:14 +03:00
parent 0831984918
commit 92bf9eaa12
6 changed files with 699 additions and 6 deletions

View File

@@ -12,6 +12,9 @@ from urllib.parse import parse_qs, quote, urljoin, urlsplit
from playwright.async_api import async_playwright
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
from reachability import find_covered # noqa: E402
async def run(url, out):
out.mkdir(parents=True, exist_ok=True)
@@ -96,6 +99,12 @@ async def run(url, out):
assert not hits, f"Text painted on text at {label}: {hits}"
report.setdefault("no_text_collision", []).append(label)
async def assert_reachable(label, selector, key_attr=None):
"""Fail when a visible control is painted over: a human could not click it."""
covered = await find_covered(page, selector, key_attr)
assert not covered, f"Unreachable controls at {label}: {covered}"
report.setdefault("reachable", []).append(label)
async def assert_dark_theme(label):
colors = await page.evaluate("""() => {
const canvas = document.createElement('canvas');
@@ -130,6 +139,11 @@ async def run(url, out):
assert report["navigation_routes"] == 9
await assert_no_overflow("desktop")
await assert_no_text_collision("feed 1440")
await assert_reachable("desktop rail 1440", "#sidebar-menu [data-nav]", "nav")
await assert_reachable(
"desktop controls 1440",
"#page-actions .btn, #view .nav-tabs .nav-link, #export-csv, #export-json, #q",
)
await assert_dark_theme("feed")
step("feed")
await page.screenshot(path=str(out / "black_feed.png"))
@@ -268,6 +282,11 @@ async def run(url, out):
await page.locator("#sidebar-toggle").click()
await page.locator("#sidebar-menu").wait_for(state="visible")
assert await page.locator('[data-nav="sources"]').is_visible()
await assert_reachable(
"mobile menu 390",
"#sidebar-menu [data-nav], .sidebar-footer button, #sidebar-toggle",
"nav",
)
await assert_no_overflow("mobile navigation 390")
await assert_dark_theme("mobile navigation")
await page.screenshot(path=str(out / "black_mobile_navigation.png"))