All checks were successful
Adds a «POI» checkbox to the terrain popup that toggles the poi-circles and poi-labels layers via map.setLayoutProperty. The choice is persisted in localStorage (key `poi-visible`) and restored on page load and after style changes, kept consistent with the runtime layerState.poi per ADR-0001. Tests: behavioral JS unit tests (TP-01..TP-04) run via `node --test`, wrapped by tests/unit/test_poi_toggle.py with static structure checks so they execute under the existing `pytest tests/` CI step. Refs: ET-002 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
163 lines
8.6 KiB
Python
163 lines
8.6 KiB
Python
"""ET-002 — тесты чекбокса показа/скрытия POI в попапе рельефа.
|
||
|
||
Изменение ET-002 — исключительно фронтендовое (`src/web/index.html`,
|
||
`src/web/app.js`). В CI исполняется только `pytest tests/`, поэтому файл
|
||
покрывает фичу двумя способами:
|
||
|
||
1. Статические проверки структуры `index.html` и `app.js` — выполняются
|
||
всегда, без внешних зависимостей.
|
||
2. Поведенческие JS unit-тесты (TP-01..TP-04 из `04-test-plan.yaml`) —
|
||
запускаются через встроенный тест-раннер Node (`node --test`). Если
|
||
`node` в системе отсутствует — эта часть помечается `skip` (по аналогии
|
||
с `tests/integration/test_routing_barriers.py::test_lua_syntax` и
|
||
`luac`).
|
||
|
||
Браузерные e2e-сценарии (TP-05..TP-09) требуют Playwright-инфраструктуры,
|
||
которой в репозитории нет; добавление новых npm-пакетов запрещено
|
||
`07-infra-requirements.md`. Их поведенческая суть покрыта JS unit-тестами
|
||
и статическими проверками ниже.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import subprocess
|
||
from pathlib import Path
|
||
from shutil import which
|
||
|
||
import pytest
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||
INDEX_HTML = REPO_ROOT / "src" / "web" / "index.html"
|
||
APP_JS = REPO_ROOT / "src" / "web" / "app.js"
|
||
JS_TEST = REPO_ROOT / "tests" / "unit" / "poi_toggle.test.js"
|
||
|
||
|
||
def _index_html() -> str:
|
||
assert INDEX_HTML.is_file(), f"не найден {INDEX_HTML}"
|
||
return INDEX_HTML.read_text(encoding="utf-8")
|
||
|
||
|
||
def _app_js() -> str:
|
||
assert APP_JS.is_file(), f"не найден {APP_JS}"
|
||
return APP_JS.read_text(encoding="utf-8")
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Статические проверки index.html (REQ-F-01, UI-спецификация)
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def test_poi_checkbox_present_in_html():
|
||
"""REQ-F-01: чекбокс POI присутствует в попапе с корректными атрибутами."""
|
||
html = _index_html()
|
||
assert 'id="poi-visible-cb"' in html, "нет чекбокса poi-visible-cb"
|
||
assert 'onchange="onPoiCheckbox()"' in html, "чекбокс не привязан к onPoiCheckbox()"
|
||
assert "<span>POI</span>" in html, "нет подписи «POI»"
|
||
|
||
|
||
def test_poi_checkbox_checked_by_default():
|
||
"""REQ-F-02: чекбокс POI отрисован как checked (POI видны по умолчанию)."""
|
||
html = _index_html()
|
||
# Атрибут checked должен стоять именно на инпуте poi-visible-cb.
|
||
start = html.index('id="poi-visible-cb"')
|
||
tag_end = html.index(">", start)
|
||
assert "checked" in html[start:tag_end], "у чекбокса POI нет атрибута checked"
|
||
|
||
|
||
def test_poi_checkbox_placed_after_trails_separated_by_hr():
|
||
"""REQ-F-01: чекбокс POI стоит после «Тропы» и отделён <hr>."""
|
||
html = _index_html()
|
||
trails_pos = html.index('id="trails-path-cb"')
|
||
poi_pos = html.index('id="poi-visible-cb"')
|
||
assert poi_pos > trails_pos, "POI должен идти после чекбокса «Тропы»"
|
||
between = html[trails_pos:poi_pos]
|
||
assert "<hr" in between, "POI не отделён горизонтальной линией <hr>"
|
||
|
||
|
||
def test_poi_checkbox_uses_shared_style_class():
|
||
"""UI-спецификация: чекбокс использует общий класс terrain-checkbox."""
|
||
html = _index_html()
|
||
start = html.index('id="poi-visible-cb"')
|
||
label_start = html.rfind("<label", 0, start)
|
||
label_open_end = html.index(">", label_start)
|
||
assert 'class="terrain-checkbox"' in html[label_start:label_open_end], (
|
||
"чекбокс POI должен быть в label с классом terrain-checkbox"
|
||
)
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Статические проверки app.js (ADR-0001)
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def test_poi_functions_defined():
|
||
"""ADR-0001: определены хелпер и обработчики POI."""
|
||
js = _app_js()
|
||
for fn in ("applyPoiVisibility", "onPoiCheckbox", "restorePoiState"):
|
||
assert f"function {fn}(" in js, f"не определена функция {fn}()"
|
||
|
||
|
||
def test_poi_logic_uses_localstorage_key():
|
||
"""REQ-F-05: персистентность через localStorage ключ poi-visible."""
|
||
js = _app_js()
|
||
assert "localStorage.setItem('poi-visible'" in js, "состояние POI не сохраняется"
|
||
assert "localStorage.getItem('poi-visible')" in js, "состояние POI не читается"
|
||
|
||
|
||
def test_poi_logic_reuses_layer_state_and_groups():
|
||
"""ADR-0001 п.3-4: источник истины — layerState.poi, группа слоёв не дублируется."""
|
||
js = _app_js()
|
||
assert "layerState.poi" in js, "POI-логика не синхронизирует layerState.poi"
|
||
assert "layerGroups.poi" in js, "POI-логика должна переиспользовать layerGroups.poi"
|
||
# poi-группа объявлена ровно один раз — в общей карте layerGroups.
|
||
assert js.count("poi: ['poi-circles', 'poi-labels']") == 1, (
|
||
"карта групп слоёв POI задублирована"
|
||
)
|
||
|
||
|
||
def test_restore_poi_state_wired_into_init():
|
||
"""REQ-F-06: restorePoiState() вызывается при инициализации/смене стиля."""
|
||
js = _app_js()
|
||
# Один def + минимум 3 вызова (rebuildMapOverlays + 2 ветки initTerrain).
|
||
assert js.count("restorePoiState()") >= 4, (
|
||
"restorePoiState() не подключён ко всем точкам восстановления"
|
||
)
|
||
|
||
|
||
def test_poi_visibility_toggled_via_set_layout_property():
|
||
"""ADR-0001 п.1: видимость переключается через setLayoutProperty."""
|
||
js = _app_js()
|
||
marker = "// >>> ET-002 POI visibility block"
|
||
end = "// <<< ET-002 POI visibility block"
|
||
assert marker in js and end in js, "маркеры POI-блока отсутствуют"
|
||
block = js[js.index(marker):js.index(end)]
|
||
assert "setLayoutProperty" in block, "POI-блок не использует setLayoutProperty"
|
||
assert "removeLayer" not in block, (
|
||
"POI-блок не должен удалять слои (ADR-0001 отвергает вариант C)"
|
||
)
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Поведенческие JS unit-тесты через Node (TP-01..TP-04)
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
node_required = pytest.mark.skipif(
|
||
which("node") is None,
|
||
reason="node не установлен — поведенческие JS unit-тесты пропущены",
|
||
)
|
||
|
||
|
||
@node_required
|
||
def test_js_unit_tests_pass():
|
||
"""TP-01..TP-04: запускает behavioral JS-тесты POI через `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-тесты POI упали (код {result.returncode}):\n"
|
||
f"{result.stdout}\n{result.stderr}"
|
||
)
|