All checks were successful
При открытии любого .bottom-sheet через openSheet() теперь принудительно скрывается #terrain-popup и снимается .active с #terrain-toggle. Это устраняет z-index конфликт (popup z=500 над sheet z=400) и убирает anti-pattern «два меню одновременно» на desktop без правки CSS-стека (marker-dialog z=500, search-panel, ruler-info — без регрессий). Реализация — Вариант A из ADR-019: helper closeTerrainPopup() + один вызов первой строкой в openSheet() после null-check. Для других sheets (sheet-route, sheet-recon, sheet-scenic, sheet-link, sheet-gpx) вызов безопасный no-op, REQ-F-06 выполняется автоматически. Тесты: - tests/unit/sheet_popup.test.js — 8 behavioral JS unit-тестов (TC-U-02, REQ-F-04, REQ-F-06 + ребра closeTerrainPopup). - tests/unit/test_sheet_popup.py — pytest-обёртка: статические проверки app.js (порядок вызовов в openSheet, маркеры блока), охранные тесты что z-stack не тронут и что gps_tracks.js/index.html не правились. Refs: ET-014 ADR: docs/work-items/ET-014/06-adr/ADR-019-terrain-popup-yields-to-sheet.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
196 lines
9.6 KiB
Python
196 lines
9.6 KiB
Python
"""ET-014 — тесты sheet ⇄ terrain-popup взаимодействия (ADR-019).
|
||
|
||
ET-014 — исключительно фронтендовое изменение (см. ADR-019): правки
|
||
`src/web/app.js`. Никаких изменений в CSS, HTML, backend, миграциях.
|
||
В CI исполняется только `pytest tests/`, поэтому файл покрывает фичу
|
||
двумя способами:
|
||
|
||
1. Статические проверки структуры `src/web/app.js` — выполняются всегда.
|
||
2. Поведенческие JS unit-тесты (TC-U-02, REQ-F-04, REQ-F-06) —
|
||
запускаются через встроенный тест-раннер Node (`node --test`). Если
|
||
`node` в системе отсутствует — эта часть помечается `skip`.
|
||
|
||
Браузерные e2e-сценарии (TC-E-01..TC-E-06, TC-UI-01..TC-UI-08) требуют
|
||
Playwright-инфраструктуры, которой в репозитории нет. Их поведенческая
|
||
суть покрыта JS unit-тестами и статическими проверками ниже.
|
||
|
||
См.:
|
||
- ADR-019: docs/work-items/ET-014/06-adr/ADR-019-terrain-popup-yields-to-sheet.md
|
||
- TRZ: docs/work-items/ET-014/02-trz.md
|
||
- AC: docs/work-items/ET-014/03-acceptance-criteria.md
|
||
- Test plan: docs/work-items/ET-014/04-test-plan.yaml
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import subprocess
|
||
from pathlib import Path
|
||
from shutil import which
|
||
|
||
import pytest
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||
APP_JS = REPO_ROOT / "src" / "web" / "app.js"
|
||
APP_CSS = REPO_ROOT / "src" / "web" / "app.css"
|
||
INDEX_HTML = REPO_ROOT / "src" / "web" / "index.html"
|
||
JS_TEST = REPO_ROOT / "tests" / "unit" / "sheet_popup.test.js"
|
||
|
||
|
||
def _read(path: Path) -> str:
|
||
assert path.is_file(), f"не найден {path}"
|
||
return path.read_text(encoding="utf-8")
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Статические проверки app.js (ADR-019)
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def test_app_js_has_et014_block_markers():
|
||
"""Блок ET-014 обрамлён маркерами для извлечения JS unit-тестами."""
|
||
js = _read(APP_JS)
|
||
assert "// >>> ET-014 sheet-popup yield block" in js, (
|
||
"нет открывающего маркера блока ET-014"
|
||
)
|
||
assert "// <<< ET-014 sheet-popup yield block <<<" in js, (
|
||
"нет закрывающего маркера блока ET-014"
|
||
)
|
||
|
||
|
||
def test_close_terrain_popup_function_defined():
|
||
"""ADR-019 §Решение/1: функция closeTerrainPopup() определена."""
|
||
js = _read(APP_JS)
|
||
assert "function closeTerrainPopup(" in js, (
|
||
"не определена функция closeTerrainPopup()"
|
||
)
|
||
|
||
|
||
def test_close_terrain_popup_inside_block():
|
||
"""closeTerrainPopup() расположена внутри ET-014-блока (для unit-тестов)."""
|
||
js = _read(APP_JS)
|
||
block_start = js.index("// >>> ET-014 sheet-popup yield block")
|
||
block_end = js.index("// <<< ET-014 sheet-popup yield block <<<")
|
||
block = js[block_start:block_end]
|
||
assert "function closeTerrainPopup(" in block, (
|
||
"closeTerrainPopup() должна быть внутри ET-014-блока"
|
||
)
|
||
|
||
|
||
def test_open_sheet_calls_close_terrain_popup_first():
|
||
"""ADR-019 §Решение/2: closeTerrainPopup() — первый вызов в openSheet()
|
||
после null-check на sheet."""
|
||
js = _read(APP_JS)
|
||
# Берём тело openSheet до первой закрывающей фигурной скобки на новой строке.
|
||
m = re.search(
|
||
r"function openSheet\(id\)\s*\{([\s\S]*?)\n\}",
|
||
js,
|
||
)
|
||
assert m, "функция openSheet(id) не найдена"
|
||
body = m.group(1)
|
||
# Проверим порядок: null-check, потом closeTerrainPopup, потом всё остальное.
|
||
nullcheck_pos = body.find("if (!sheet) return;")
|
||
close_popup_pos = body.find("closeTerrainPopup()")
|
||
close_sheet_pos = body.find("closeSheet(")
|
||
add_open_pos = body.find("classList.add('open')")
|
||
|
||
assert nullcheck_pos >= 0, "null-check на sheet в openSheet() отсутствует"
|
||
assert close_popup_pos > nullcheck_pos, (
|
||
"closeTerrainPopup() должна вызываться ПОСЛЕ null-check"
|
||
)
|
||
assert close_sheet_pos > close_popup_pos, (
|
||
"closeTerrainPopup() должна вызываться ДО закрытия других sheets"
|
||
)
|
||
assert add_open_pos > close_popup_pos, (
|
||
"closeTerrainPopup() должна вызываться ДО classList.add('open')"
|
||
)
|
||
|
||
|
||
def test_open_sheet_calls_close_terrain_popup_exactly_once():
|
||
"""REQ-NF-02: никакого дублирования вызовов (не должно быть лишних
|
||
обработчиков). closeTerrainPopup() вызывается ровно один раз в openSheet."""
|
||
js = _read(APP_JS)
|
||
m = re.search(
|
||
r"function openSheet\(id\)\s*\{([\s\S]*?)\n\}",
|
||
js,
|
||
)
|
||
assert m, "функция openSheet(id) не найдена"
|
||
body = m.group(1)
|
||
calls = body.count("closeTerrainPopup()")
|
||
assert calls == 1, (
|
||
f"closeTerrainPopup() должна вызываться ровно один раз в openSheet(), "
|
||
f"найдено {calls}"
|
||
)
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Статические проверки: что НЕ меняется (ADR-019 §Что НЕ меняется)
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def test_z_index_stack_unchanged_for_affected_widgets():
|
||
"""ADR-019 §Что НЕ меняется: z-index ключевых виджетов из конфликта
|
||
(.bottom-sheet, #sheet-backdrop, .terrain-popup, #marker-dialog)
|
||
остаётся неизменным. Эти значения — фундамент аргументации ADR-019
|
||
(Вариант A не правит CSS), любая их правка ломает обоснование.
|
||
|
||
REQ-NF-03: marker-dialog (z=500) сохраняется на верху относительно sheet'ов.
|
||
"""
|
||
css = _read(APP_CSS)
|
||
expected = [
|
||
(".bottom-sheet", "z-index: 400"),
|
||
("#sheet-backdrop", "z-index: 390"),
|
||
("#marker-dialog", "z-index: 500"),
|
||
(".terrain-popup", "z-index: 500"),
|
||
]
|
||
for selector, z in expected:
|
||
sel_pos = css.find(selector)
|
||
assert sel_pos >= 0, f"селектор {selector} не найден в app.css"
|
||
# Смотрим в окне 600 символов после селектора (CSS-блок укладывается).
|
||
window = css[sel_pos:sel_pos + 600]
|
||
assert z in window, (
|
||
f"в блоке {selector} отсутствует {z}; ADR-019 запрещает менять z-stack"
|
||
)
|
||
|
||
|
||
def test_gps_tracks_js_not_touched_by_et014():
|
||
"""ADR-019 §Что НЕ меняется: src/web/gps_tracks.js не правится ET-014."""
|
||
gps = _read(REPO_ROOT / "src" / "web" / "gps_tracks.js")
|
||
# Маркеров ET-014 в gps_tracks.js не должно быть — логика живёт в openSheet.
|
||
assert "ET-014" not in gps, (
|
||
"ET-014 не должен изменять src/web/gps_tracks.js (см. ADR-019)"
|
||
)
|
||
|
||
|
||
def test_index_html_not_touched_by_et014():
|
||
"""ADR-019 §Что НЕ меняется: src/web/index.html без изменений."""
|
||
html = _read(INDEX_HTML)
|
||
assert "ET-014" not in html, (
|
||
"ET-014 не должен изменять src/web/index.html (см. ADR-019)"
|
||
)
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Поведенческие JS unit-тесты через Node (TC-U-02, REQ-F-04, REQ-F-06)
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
node_required = pytest.mark.skipif(
|
||
which("node") is None,
|
||
reason="node не установлен — поведенческие JS unit-тесты пропущены",
|
||
)
|
||
|
||
|
||
@node_required
|
||
def test_js_unit_tests_pass():
|
||
"""TC-U-02 / REQ-F-04 / REQ-F-06: behavioral JS-тесты через `node --test`."""
|
||
assert JS_TEST.is_file(), f"не найден JS-тест {JS_TEST}"
|
||
node = which("node")
|
||
result = subprocess.run(
|
||
[node, "--test", str(JS_TEST)],
|
||
capture_output=True,
|
||
text=True,
|
||
cwd=str(REPO_ROOT),
|
||
)
|
||
assert result.returncode == 0, (
|
||
f"JS unit-тесты ET-014 упали (код {result.returncode}):\n"
|
||
f"{result.stdout}\n{result.stderr}"
|
||
)
|