"""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 "POI" 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 стоит после «Тропы» и отделён
.""" 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 "" 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_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}" )