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