Files
enduro-trails/tests/unit/test_poi_toggle.py
claude-bot 8c17a4f508
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 5s
CI / build (push) Successful in 15s
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 4s
CI / build (pull_request) Successful in 3s
feat(web): add POI visibility checkbox to terrain popup
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>
2026-05-21 15:50:54 +00:00

163 lines
8.6 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.
"""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}"
)