feat(web): add POI visibility checkbox to terrain popup
All checks were successful
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>
This commit is contained in:
@@ -11,3 +11,5 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|||||||
- Agent system prompts (architect, developer, reviewer, tester, deployer)
|
- Agent system prompts (architect, developer, reviewer, tester, deployer)
|
||||||
- CI pipeline (Gitea Actions)
|
- CI pipeline (Gitea Actions)
|
||||||
- Docker configuration
|
- Docker configuration
|
||||||
|
- ET-002: чекбокс «POI» в попапе рельефа — показ/скрытие маркеров POI
|
||||||
|
с сохранением состояния в localStorage (ключ `poi-visible`)
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ function rebuildMapOverlays() {
|
|||||||
// Re-apply terrain and trails after style change
|
// Re-apply terrain and trails after style change
|
||||||
restoreTerrainState();
|
restoreTerrainState();
|
||||||
restoreTrailsState();
|
restoreTrailsState();
|
||||||
|
restorePoiState();
|
||||||
|
|
||||||
// Re-apply recon circle if active
|
// Re-apply recon circle if active
|
||||||
if (reconMode && reconCenter) {
|
if (reconMode && reconCenter) {
|
||||||
doRecon(reconCenter[0], reconCenter[1]);
|
doRecon(reconCenter[0], reconCenter[1]);
|
||||||
@@ -2796,6 +2797,62 @@ function restoreTrailsState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// >>> ET-002 POI visibility block (do not remove markers — used by unit tests) >>>
|
||||||
|
// Видимость POI (слои poi-circles, poi-labels) управляется чекбоксом
|
||||||
|
// «POI» в попапе рельефа. Состояние хранится в localStorage под ключом
|
||||||
|
// 'poi-visible' ('1'/'0'). Источник истины в рантайме — layerState.poi.
|
||||||
|
// См. docs/work-items/ET-002/06-adr/adr-0001-poi-visibility-client-side.md
|
||||||
|
/**
|
||||||
|
* Применяет видимость группы слоёв POI и синхронизирует layerState.poi.
|
||||||
|
*
|
||||||
|
* Единый приватный хелпер: переиспользуется чекбоксом попапа
|
||||||
|
* (onPoiCheckbox) и восстановлением состояния при загрузке/смене стиля
|
||||||
|
* (restorePoiState). Не пишет в localStorage — персистентность остаётся
|
||||||
|
* ответственностью обработчика чекбокса.
|
||||||
|
*
|
||||||
|
* @param {boolean} visible - true — показать POI, false — скрыть.
|
||||||
|
*/
|
||||||
|
function applyPoiVisibility(visible) {
|
||||||
|
layerState.poi = visible;
|
||||||
|
const map = window._map;
|
||||||
|
if (!map) return;
|
||||||
|
const visibility = visible ? 'visible' : 'none';
|
||||||
|
layerGroups.poi.forEach(id => {
|
||||||
|
if (map.getLayer(id)) {
|
||||||
|
map.setLayoutProperty(id, 'visibility', visibility);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик чекбокса «POI» в попапе рельефа (атрибут onchange).
|
||||||
|
*
|
||||||
|
* Сохраняет выбор в localStorage ('poi-visible': '1' видимы | '0' скрыты)
|
||||||
|
* и применяет видимость слоёв POI через applyPoiVisibility().
|
||||||
|
*/
|
||||||
|
function onPoiCheckbox() {
|
||||||
|
const checked = document.getElementById('poi-visible-cb').checked;
|
||||||
|
localStorage.setItem('poi-visible', checked ? '1' : '0');
|
||||||
|
applyPoiVisibility(checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Восстанавливает видимость POI при загрузке страницы и после смены
|
||||||
|
* стиля карты (переключение темы).
|
||||||
|
*
|
||||||
|
* По умолчанию (ключ 'poi-visible' отсутствует или равен '1') POI
|
||||||
|
* видимы; '0' — скрыты. Синхронизирует чекбокс, layerState.poi и
|
||||||
|
* фактическую видимость слоёв.
|
||||||
|
*/
|
||||||
|
function restorePoiState() {
|
||||||
|
const stored = localStorage.getItem('poi-visible');
|
||||||
|
const poiOn = stored === null || stored === '1';
|
||||||
|
const cb = document.getElementById('poi-visible-cb');
|
||||||
|
if (cb) cb.checked = poiOn;
|
||||||
|
applyPoiVisibility(poiOn);
|
||||||
|
}
|
||||||
|
// <<< ET-002 POI visibility block <<<
|
||||||
|
|
||||||
function applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom) {
|
function applyTerrainLayer(id, tileUrl, enabled, opacity, minzoom, maxzoom) {
|
||||||
const map = window._map;
|
const map = window._map;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
@@ -2890,6 +2947,7 @@ function restoreTerrainState() {
|
|||||||
// Initial state
|
// Initial state
|
||||||
restoreTerrainState();
|
restoreTerrainState();
|
||||||
restoreTrailsState();
|
restoreTrailsState();
|
||||||
|
restorePoiState();
|
||||||
} else {
|
} else {
|
||||||
// Map not ready yet, wait
|
// Map not ready yet, wait
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
@@ -2902,6 +2960,7 @@ function restoreTerrainState() {
|
|||||||
updateHillshadeAvailability();
|
updateHillshadeAvailability();
|
||||||
restoreTerrainState();
|
restoreTerrainState();
|
||||||
restoreTrailsState();
|
restoreTrailsState();
|
||||||
|
restorePoiState();
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,11 @@
|
|||||||
<input type="checkbox" id="trails-path-cb" onchange="onTrailsCheckbox()" checked>
|
<input type="checkbox" id="trails-path-cb" onchange="onTrailsCheckbox()" checked>
|
||||||
<span>Тропы</span>
|
<span>Тропы</span>
|
||||||
</label>
|
</label>
|
||||||
|
<hr style="margin:6px 0;border-color:rgba(128,128,128,0.3)">
|
||||||
|
<label class="terrain-checkbox">
|
||||||
|
<input type="checkbox" id="poi-visible-cb" onchange="onPoiCheckbox()" checked>
|
||||||
|
<span>POI</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Map Buttons (right) ───────────────── -->
|
<!-- ── Map Buttons (right) ───────────────── -->
|
||||||
|
|||||||
167
tests/unit/poi_toggle.test.js
Normal file
167
tests/unit/poi_toggle.test.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ET-002 — поведенческие unit-тесты чекбокса видимости POI.
|
||||||
|
*
|
||||||
|
* Покрывают TP-01..TP-04 из docs/work-items/ET-002/04-test-plan.yaml.
|
||||||
|
*
|
||||||
|
* Тесты исполняют РЕАЛЬНЫЙ код из src/web/app.js: блок POI извлекается
|
||||||
|
* по маркерам `>>> ET-002 POI visibility block` и оборачивается в
|
||||||
|
* фабрику через `new Function`, которой передаются мок-зависимости
|
||||||
|
* (window, document, localStorage, layerState, layerGroups). Так
|
||||||
|
* монолитный browser-скрипт проверяется без полной загрузки в Node.
|
||||||
|
*
|
||||||
|
* Запуск: `node --test tests/unit/poi_toggle.test.js`
|
||||||
|
* (в CI оборачивается pytest-тестом tests/unit/test_poi_toggle.py).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const APP_JS = path.join(__dirname, '..', '..', 'src', 'web', 'app.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Извлекает блок POI-логики из app.js и собирает из него модуль,
|
||||||
|
* подставляя переданные зависимости.
|
||||||
|
*/
|
||||||
|
function loadPoiModule(deps) {
|
||||||
|
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||||
|
const m = src.match(
|
||||||
|
/\/\/ >>> ET-002 POI visibility block[^\n]*\n([\s\S]*?)\/\/ <<< ET-002 POI visibility block/
|
||||||
|
);
|
||||||
|
assert.ok(m, 'POI-блок не найден в app.js (маркеры ET-002 отсутствуют)');
|
||||||
|
const factory = new Function(
|
||||||
|
'layerState', 'layerGroups', 'window', 'document', 'localStorage',
|
||||||
|
m[1] + '\nreturn { applyPoiVisibility, onPoiCheckbox, restorePoiState };'
|
||||||
|
);
|
||||||
|
return factory(
|
||||||
|
deps.layerState, deps.layerGroups, deps.window, deps.document, deps.localStorage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Готовит изолированное мок-окружение для одного теста.
|
||||||
|
* @param {object} [opts]
|
||||||
|
* @param {string} [opts.stored] - значение ключа poi-visible в localStorage
|
||||||
|
* (если не указан — ключ отсутствует).
|
||||||
|
* @param {boolean} [opts.layerExists] - что возвращает map.getLayer().
|
||||||
|
*/
|
||||||
|
function makeEnv({ stored, layerExists = true } = {}) {
|
||||||
|
const calls = { setLayoutProperty: [], setItem: [] };
|
||||||
|
const store = {};
|
||||||
|
if (stored !== undefined) store['poi-visible'] = stored;
|
||||||
|
|
||||||
|
const localStorage = {
|
||||||
|
getItem: (k) => (k in store ? store[k] : null),
|
||||||
|
setItem: (k, v) => { store[k] = String(v); calls.setItem.push([k, String(v)]); },
|
||||||
|
};
|
||||||
|
const map = {
|
||||||
|
getLayer: () => layerExists,
|
||||||
|
setLayoutProperty: (id, prop, val) => calls.setLayoutProperty.push([id, prop, val]),
|
||||||
|
};
|
||||||
|
const checkbox = { checked: true };
|
||||||
|
const document = {
|
||||||
|
getElementById: (id) => (id === 'poi-visible-cb' ? checkbox : null),
|
||||||
|
};
|
||||||
|
const layerState = { tracks: true, paths: true, poi: true, basemap: true };
|
||||||
|
const layerGroups = { poi: ['poi-circles', 'poi-labels'] };
|
||||||
|
const win = { _map: map };
|
||||||
|
|
||||||
|
const mod = loadPoiModule({
|
||||||
|
layerState, layerGroups, window: win, document, localStorage,
|
||||||
|
});
|
||||||
|
return { mod, calls, checkbox, layerState, store };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TP-01: onPoiCheckbox() скрывает слои при checked=false ───────────────
|
||||||
|
test('TP-01: снятый чекбокс скрывает слои POI и сохраняет 0', () => {
|
||||||
|
const env = makeEnv();
|
||||||
|
env.checkbox.checked = false;
|
||||||
|
|
||||||
|
env.mod.onPoiCheckbox();
|
||||||
|
|
||||||
|
assert.deepEqual(env.calls.setLayoutProperty, [
|
||||||
|
['poi-circles', 'visibility', 'none'],
|
||||||
|
['poi-labels', 'visibility', 'none'],
|
||||||
|
]);
|
||||||
|
assert.deepEqual(env.calls.setItem, [['poi-visible', '0']]);
|
||||||
|
assert.equal(env.layerState.poi, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── TP-02: onPoiCheckbox() показывает слои при checked=true ──────────────
|
||||||
|
test('TP-02: установленный чекбокс показывает слои POI и сохраняет 1', () => {
|
||||||
|
const env = makeEnv();
|
||||||
|
env.checkbox.checked = true;
|
||||||
|
|
||||||
|
env.mod.onPoiCheckbox();
|
||||||
|
|
||||||
|
assert.deepEqual(env.calls.setLayoutProperty, [
|
||||||
|
['poi-circles', 'visibility', 'visible'],
|
||||||
|
['poi-labels', 'visibility', 'visible'],
|
||||||
|
]);
|
||||||
|
assert.deepEqual(env.calls.setItem, [['poi-visible', '1']]);
|
||||||
|
assert.equal(env.layerState.poi, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── TP-03: восстановление состояния — POI скрыты ─────────────────────────
|
||||||
|
test('TP-03: restorePoiState() при poi-visible=0 скрывает POI', () => {
|
||||||
|
const env = makeEnv({ stored: '0' });
|
||||||
|
|
||||||
|
env.mod.restorePoiState();
|
||||||
|
|
||||||
|
assert.equal(env.checkbox.checked, false);
|
||||||
|
assert.equal(env.layerState.poi, false);
|
||||||
|
assert.deepEqual(env.calls.setLayoutProperty, [
|
||||||
|
['poi-circles', 'visibility', 'none'],
|
||||||
|
['poi-labels', 'visibility', 'none'],
|
||||||
|
]);
|
||||||
|
// restore не должен переписывать localStorage
|
||||||
|
assert.deepEqual(env.calls.setItem, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── TP-04: восстановление состояния — POI видны (default) ────────────────
|
||||||
|
test('TP-04: restorePoiState() без ключа включает POI по умолчанию', () => {
|
||||||
|
const env = makeEnv();
|
||||||
|
|
||||||
|
env.mod.restorePoiState();
|
||||||
|
|
||||||
|
assert.equal(env.checkbox.checked, true);
|
||||||
|
assert.equal(env.layerState.poi, true);
|
||||||
|
assert.deepEqual(env.calls.setLayoutProperty, [
|
||||||
|
['poi-circles', 'visibility', 'visible'],
|
||||||
|
['poi-labels', 'visibility', 'visible'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Доп.: значение '1' восстанавливает видимость явно ────────────────────
|
||||||
|
test('restorePoiState() при poi-visible=1 показывает POI', () => {
|
||||||
|
const env = makeEnv({ stored: '1' });
|
||||||
|
|
||||||
|
env.mod.restorePoiState();
|
||||||
|
|
||||||
|
assert.equal(env.checkbox.checked, true);
|
||||||
|
assert.equal(env.layerState.poi, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Доп.: POI-логика не трогает чужие слои (дух TP-08) ───────────────────
|
||||||
|
test('onPoiCheckbox() меняет только слои poi-circles и poi-labels', () => {
|
||||||
|
const env = makeEnv();
|
||||||
|
env.checkbox.checked = false;
|
||||||
|
|
||||||
|
env.mod.onPoiCheckbox();
|
||||||
|
|
||||||
|
const touched = env.calls.setLayoutProperty.map((c) => c[0]);
|
||||||
|
assert.deepEqual([...touched].sort(), ['poi-circles', 'poi-labels']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Доп.: layerState синхронизируется даже без слоёв на карте ────────────
|
||||||
|
test('applyPoiVisibility() обновляет layerState даже если слой ещё не добавлен', () => {
|
||||||
|
const env = makeEnv({ layerExists: false });
|
||||||
|
|
||||||
|
env.mod.applyPoiVisibility(false);
|
||||||
|
|
||||||
|
assert.equal(env.layerState.poi, false);
|
||||||
|
assert.deepEqual(env.calls.setLayoutProperty, []);
|
||||||
|
});
|
||||||
162
tests/unit/test_poi_toggle.py
Normal file
162
tests/unit/test_poi_toggle.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"""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}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user