Files
enduro-trails/tests/unit/poi_toggle.test.js
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

168 lines
6.8 KiB
JavaScript

'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, []);
});