All checks were successful
12-review.md (REQUEST_CHANGES, attempt 2/3) flagged 6 must-fix items
in the analysis/architecture artefacts plus matching bugs that had
already leaked into the committed implementation. This patch lands
both: documents corrected, code aligned with corrected specs, tests
updated.
P1-1: TRZ/ADR/Data/Risks referenced fictional layer ids
(`trails-grade1..5-halo-satellite`, `paths-bridleway-halo-satellite`).
Actual style*.json has only `trails-track-halo-satellite` and
`trails-path-bridleway-halo-satellite`; grade differentiation lives
inside one `match` expression on `tracktype` within `trails-track`.
Docs rewritten to operate on real ids.
P1-2: POI labels contrast was broken — spec changed only halo-color
to black, leaving `text-color: #333333` (light theme baseline)
unreadable over the new black halo. Code+docs now switch BOTH
`text-color` (-> `#ffffff` on satellite) AND halo together, with
per-theme baselines (`#333333` light / `#e0e0e0` dark) restored on
return to Schematic.
P1-3: BRD §5 hillshade risk said «hillshade auto-disabled on
satellite», contradicting TRZ/ADR/AC. BRD wording aligned: hillshade
keeps working over satellite; visual check is AC-04.
P1-4: background-color had four divergent sources (`#1a1a1a`,
`#2a2a2a`, `#1a1a2e`, `#f0ede6`), incl. an inverted-theme typo and a
baseline `#1a1a1a` that didn't match the actual `style-dark.json:28`
value `#1a1a2e`. Settled on ADR-004's single-constant model: `#2a2a2a`
on satellite for both themes; on Schematic restore per-theme baselines
`#f0ede6` (light) / `#1a1a2e` (dark). `_applyBackgroundForSatellite`
fixed accordingly.
P1-5: app.js already had `layerState.basemap` and `toggleLayer
('basemap')` (legacy «Базовая карта» switch). Neither TRZ nor ADR
specified the interaction. Added save&restore contract: on entering
Satellite save `layerState.basemap` to `_savedBasemapState` and
force-hide `osm-base`; on returning to Schematic restore osm-base
visibility from the saved value. CSS hook `body.satellite-active
#btn-basemap { display:none }` keeps the user from trying to enable
a hybrid mode (out of scope, BRD §3). TRZ §5.6, ADR-004 §8.
P1-6: `restoreTrailsState()` and `onTrailsCheckbox()` only managed
visibility of `trails-track` / `trails-path-bridleway`, leaving
their halo-underlay siblings as «phantom» halos when the user
unchecked grunты/тропы under Satellite. Introduced
`_applyTrailHaloVisibility(map, base)` reading checkbox state from
DOM; called from `onTrailsCheckbox`, `restoreTrailsState`, and both
branches of `applyBaseLayer`. Rule: halo visible ⇔ (base ===
satellite) AND (checkbox ON). TRZ §5.7, ADR-004 §9.
Docs bumped: BRD v2, TRZ v2, AC v2, Data v2, Risks v2; ADR-004
получает «Ревизии»-секцию (status remains accepted — only editorial
fixes, no decision change).
Tests:
- tests/unit/base_layer.test.js: rewritten 2 background-color
assertions (#1a1a1a expectation removed), added 6 new tests for
P1-2 / P1-4 (POI text-color per-theme baselines, single satellite
bg #2a2a2a, baseline restore on Schematic).
- All 33 JS unit tests + 22 pytest static checks green.
- Full pytest suite: 76 passed (excluding pre-existing
shapely-import skipped collection in tests/unit/test_health.py).
Refs: ET-007
Review: docs/work-items/ET-007/12-review.md (P1-1..P1-6)
ADR: docs/work-items/ET-007/06-adr/ADR-004-satellite-base-layer.md (rev. 2026-05-31)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
469 lines
22 KiB
JavaScript
469 lines
22 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* ET-007 — поведенческие unit-тесты переключателя подложки «Схема / Спутник».
|
||
*
|
||
* Покрывают U-01..U-05 и U-10..U-11 из docs/work-items/ET-007/04-test-plan.yaml,
|
||
* а также часть интеграционных кейсов (I-01..I-04, I-06, I-07, I-24, I-25),
|
||
* проверяемых на мок-карте.
|
||
*
|
||
* Тесты исполняют РЕАЛЬНЫЙ код из блока ET-007 в src/web/app.js: блок
|
||
* извлекается по маркерам `>>> ET-007 base layer toggle block` и
|
||
* оборачивается в фабрику через `new Function`, которой передаются
|
||
* мок-зависимости (window, document, localStorage). Так монолитный
|
||
* browser-скрипт проверяется без полной загрузки в Node.
|
||
*
|
||
* Запуск: `node --test tests/unit/base_layer.test.js`
|
||
* (в CI оборачивается pytest-тестом tests/unit/test_base_layer.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');
|
||
|
||
/**
|
||
* Извлекает блок ET-007 из app.js и собирает из него модуль,
|
||
* подставляя переданные зависимости.
|
||
*/
|
||
function loadBaseLayerModule(deps) {
|
||
const src = fs.readFileSync(APP_JS, 'utf8');
|
||
const m = src.match(
|
||
/\/\/ >>> ET-007 base layer toggle block[^\n]*\n([\s\S]*?)\/\/ <<< ET-007 base layer toggle block/
|
||
);
|
||
assert.ok(m, 'ET-007-блок не найден в app.js (маркеры отсутствуют)');
|
||
const factory = new Function(
|
||
'window', 'document',
|
||
m[1] + '\nreturn { getStoredBaseLayer, onBaseLayerToggle, applyBaseLayer, restoreBaseLayerState, syncBaseLayerUI, _firstOverlayLayerId, SATELLITE_SOURCE_ID, SATELLITE_LAYER_ID, SATELLITE_TILE_URL, SATELLITE_ATTRIBUTION, SATELLITE_HALO_LAYER_IDS };'
|
||
);
|
||
return factory(deps.window, deps.document);
|
||
}
|
||
|
||
/**
|
||
* Готовит изолированное мок-окружение для одного теста.
|
||
*
|
||
* Создаёт мок-карту с журналом вызовов (addSource/addLayer/
|
||
* setLayoutProperty/setPaintProperty), мок-DOM с кнопками
|
||
* #base-btn-schematic и #base-btn-satellite, мок-localStorage и
|
||
* мок-document.body.classList для определения активной темы.
|
||
*/
|
||
function makeEnv({
|
||
stored,
|
||
noStorage = false,
|
||
layers = ['background', 'osm-base', 'trails-track-halo-satellite', 'trails-track', 'trails-path-bridleway-halo-satellite', 'trails-path-bridleway', 'poi-circles', 'poi-labels'],
|
||
themeDark = false,
|
||
noMap = false,
|
||
} = {}) {
|
||
const calls = {
|
||
addSource: [],
|
||
addLayer: [],
|
||
setLayoutProperty: [],
|
||
setPaintProperty: [],
|
||
setItem: [],
|
||
};
|
||
const store = {};
|
||
if (stored !== undefined) store['map-base-layer'] = 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 sourceSet = new Set();
|
||
const layerSet = new Set(layers);
|
||
const layoutByLayer = {};
|
||
const map = {
|
||
getSource: (id) => (sourceSet.has(id) ? { id } : undefined),
|
||
addSource: (id, spec) => { sourceSet.add(id); calls.addSource.push([id, spec]); },
|
||
getLayer: (id) => (layerSet.has(id) ? { id } : undefined),
|
||
addLayer: (spec, before) => {
|
||
layerSet.add(spec.id);
|
||
if (spec.layout && spec.layout.visibility) {
|
||
layoutByLayer[spec.id] = layoutByLayer[spec.id] || {};
|
||
layoutByLayer[spec.id].visibility = spec.layout.visibility;
|
||
}
|
||
calls.addLayer.push([spec, before]);
|
||
},
|
||
setLayoutProperty: (id, prop, val) => {
|
||
layoutByLayer[id] = layoutByLayer[id] || {};
|
||
layoutByLayer[id][prop] = val;
|
||
calls.setLayoutProperty.push([id, prop, val]);
|
||
},
|
||
setPaintProperty: (id, prop, val) => {
|
||
calls.setPaintProperty.push([id, prop, val]);
|
||
},
|
||
getLayoutProperty: (id, prop) => (layoutByLayer[id] || {})[prop],
|
||
getStyle: () => ({ layers: layers.map((id) => ({ id })) }),
|
||
};
|
||
|
||
const schBtn = { classList: { _classes: new Set(['seg-btn', 'active']),
|
||
toggle(name, on) { if (on) this._classes.add(name); else this._classes.delete(name); },
|
||
contains(n) { return this._classes.has(n); } } };
|
||
const satBtn = { classList: { _classes: new Set(['seg-btn']),
|
||
toggle(name, on) { if (on) this._classes.add(name); else this._classes.delete(name); },
|
||
contains(n) { return this._classes.has(n); } } };
|
||
const bodyClasses = new Set(themeDark ? ['theme-dark'] : ['theme-light']);
|
||
const document = {
|
||
getElementById: (id) => {
|
||
if (id === 'base-btn-schematic') return schBtn;
|
||
if (id === 'base-btn-satellite') return satBtn;
|
||
return null;
|
||
},
|
||
body: {
|
||
classList: {
|
||
contains: (c) => bodyClasses.has(c),
|
||
},
|
||
},
|
||
};
|
||
const win = noMap
|
||
? { localStorage }
|
||
: (noStorage
|
||
? { _map: map, get localStorage() { throw new Error('localStorage disabled'); } }
|
||
: { _map: map, localStorage });
|
||
|
||
const mod = loadBaseLayerModule({ window: win, document });
|
||
return { mod, calls, store, schBtn, satBtn, map, sourceSet, layerSet, layoutByLayer };
|
||
}
|
||
|
||
// ── U-01: Default — Схема, если localStorage пуст ─────────────────────
|
||
test('U-01: без сохранённого значения getStoredBaseLayer() возвращает "schematic"', () => {
|
||
const env = makeEnv();
|
||
assert.equal(env.mod.getStoredBaseLayer(), 'schematic');
|
||
});
|
||
|
||
test('U-01: restoreBaseLayerState() при пустом localStorage активирует «Схему»', () => {
|
||
const env = makeEnv();
|
||
env.mod.restoreBaseLayerState();
|
||
|
||
assert.ok(env.schBtn.classList.contains('active'));
|
||
assert.ok(!env.satBtn.classList.contains('active'));
|
||
// На схеме спутниковый источник НЕ создаётся.
|
||
assert.deepEqual(env.calls.addSource, []);
|
||
assert.deepEqual(env.calls.addLayer, []);
|
||
});
|
||
|
||
// ── U-02: Чтение значения 'satellite' из localStorage ─────────────────
|
||
test('U-02: restoreBaseLayerState() при stored=satellite активирует «Спутник»', () => {
|
||
const env = makeEnv({ stored: 'satellite' });
|
||
env.mod.restoreBaseLayerState();
|
||
|
||
assert.ok(env.satBtn.classList.contains('active'));
|
||
assert.ok(!env.schBtn.classList.contains('active'));
|
||
// Создан спутниковый source/layer.
|
||
assert.equal(env.calls.addSource.length, 1);
|
||
assert.equal(env.calls.addSource[0][0], 'satellite-raster');
|
||
assert.equal(env.calls.addLayer.length, 1);
|
||
assert.equal(env.calls.addLayer[0][0].id, 'satellite-base');
|
||
});
|
||
|
||
// ── U-03: Запись значения при переключении ────────────────────────────
|
||
test('U-03: onBaseLayerToggle("satellite") пишет в localStorage', () => {
|
||
const env = makeEnv();
|
||
env.mod.onBaseLayerToggle('satellite');
|
||
assert.deepEqual(env.calls.setItem, [['map-base-layer', 'satellite']]);
|
||
assert.equal(env.store['map-base-layer'], 'satellite');
|
||
});
|
||
|
||
// ── U-04: Игнор некорректного значения в localStorage ─────────────────
|
||
test('U-04: некорректное stored значение даёт дефолт "schematic"', () => {
|
||
const env = makeEnv({ stored: 'unknown' });
|
||
assert.equal(env.mod.getStoredBaseLayer(), 'schematic');
|
||
});
|
||
|
||
test('U-04: пустая строка трактуется как дефолт', () => {
|
||
const env = makeEnv({ stored: '' });
|
||
assert.equal(env.mod.getStoredBaseLayer(), 'schematic');
|
||
});
|
||
|
||
// ── U-05: Toggle на уже активный режим — no-op ────────────────────────
|
||
test('U-05: повторный onBaseLayerToggle("schematic") при активной схеме — no-op', () => {
|
||
const env = makeEnv();
|
||
env.mod.onBaseLayerToggle('schematic');
|
||
assert.deepEqual(env.calls.setItem, []);
|
||
assert.deepEqual(env.calls.addSource, []);
|
||
assert.deepEqual(env.calls.addLayer, []);
|
||
assert.deepEqual(env.calls.setLayoutProperty, []);
|
||
});
|
||
|
||
test('U-05: повторный onBaseLayerToggle("satellite") при активном спутнике — no-op', () => {
|
||
const env = makeEnv({ stored: 'satellite' });
|
||
// первый вызов вернёт source/layer
|
||
env.mod.restoreBaseLayerState();
|
||
const setLayoutBefore = env.calls.setLayoutProperty.length;
|
||
const setItemBefore = env.calls.setItem.length;
|
||
|
||
env.mod.onBaseLayerToggle('satellite');
|
||
|
||
// никаких новых обращений
|
||
assert.equal(env.calls.setLayoutProperty.length, setLayoutBefore);
|
||
assert.equal(env.calls.setItem.length, setItemBefore);
|
||
});
|
||
|
||
// ── U-10..U-11: syncBaseLayerUI() ─────────────────────────────────────
|
||
test('U-10: syncBaseLayerUI("satellite") переносит .active на «Спутник»', () => {
|
||
const env = makeEnv();
|
||
env.mod.syncBaseLayerUI('satellite');
|
||
assert.ok(env.satBtn.classList.contains('active'));
|
||
assert.ok(!env.schBtn.classList.contains('active'));
|
||
});
|
||
|
||
test('U-11: syncBaseLayerUI("schematic") переносит .active на «Схему»', () => {
|
||
const env = makeEnv();
|
||
// сначала сделаем спутник активным
|
||
env.mod.syncBaseLayerUI('satellite');
|
||
env.mod.syncBaseLayerUI('schematic');
|
||
assert.ok(env.schBtn.classList.contains('active'));
|
||
assert.ok(!env.satBtn.classList.contains('active'));
|
||
});
|
||
|
||
// ── I-01..I-02: спутниковый source/layer создаются при первом включении ─
|
||
test('I-01: applyBaseLayer("satellite") добавляет source satellite-raster (Esri URL)', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
assert.equal(env.calls.addSource.length, 1);
|
||
const [id, spec] = env.calls.addSource[0];
|
||
assert.equal(id, 'satellite-raster');
|
||
assert.equal(spec.type, 'raster');
|
||
assert.equal(spec.tileSize, 256);
|
||
assert.ok(spec.tiles[0].includes('arcgisonline.com'));
|
||
});
|
||
|
||
test('I-02: applyBaseLayer("satellite") добавляет layer satellite-base типа raster', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
const [spec, before] = env.calls.addLayer[0];
|
||
assert.equal(spec.id, 'satellite-base');
|
||
assert.equal(spec.type, 'raster');
|
||
assert.equal(spec.source, 'satellite-raster');
|
||
// beforeId — первый terrain/trails/poi слой; в наборе по умолчанию это
|
||
// trails-track-halo-satellite (первый с префиксом trails-/poi-/terrain-).
|
||
assert.equal(before, 'trails-track-halo-satellite');
|
||
});
|
||
|
||
// ── I-03: visibility osm-base скрыт после переключения на спутник ──────
|
||
test('I-03: osm-base скрывается при включении спутника', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
const osmCalls = env.calls.setLayoutProperty.filter((c) => c[0] === 'osm-base');
|
||
assert.ok(osmCalls.some((c) => c[1] === 'visibility' && c[2] === 'none'));
|
||
});
|
||
|
||
// ── I-04: возврат на схему — osm-base видим, satellite-base скрыт ──────
|
||
test('I-04: возврат на «Схему» возвращает osm-base в visible и скрывает satellite-base', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
env.calls.setLayoutProperty.length = 0;
|
||
env.mod.applyBaseLayer('schematic');
|
||
|
||
const osmVisible = env.calls.setLayoutProperty.find(
|
||
(c) => c[0] === 'osm-base' && c[1] === 'visibility' && c[2] === 'visible'
|
||
);
|
||
const satHidden = env.calls.setLayoutProperty.find(
|
||
(c) => c[0] === 'satellite-base' && c[1] === 'visibility' && c[2] === 'none'
|
||
);
|
||
assert.ok(osmVisible, 'osm-base не возвращён в visible');
|
||
assert.ok(satHidden, 'satellite-base не скрыт при возврате на схему');
|
||
// source НЕ удаляется (TRZ §1 REQ-F-03).
|
||
assert.ok(env.sourceSet.has('satellite-raster'));
|
||
});
|
||
|
||
// ── I-07: атрибуция Esri зарегистрирована ──────────────────────────────
|
||
test('I-07: source содержит атрибуцию Esri', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
const [, spec] = env.calls.addSource[0];
|
||
assert.ok(/Esri/.test(spec.attribution),
|
||
'attribution source не упоминает Esri');
|
||
});
|
||
|
||
// ── I-23/halo: halo-underlay-слои включаются на спутнике ──────────────
|
||
test('halo-underlay-слои включаются при «Спутнике» и скрываются при «Схеме»', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
const trackHaloOn = env.calls.setLayoutProperty.find(
|
||
(c) => c[0] === 'trails-track-halo-satellite' && c[2] === 'visible'
|
||
);
|
||
const pathHaloOn = env.calls.setLayoutProperty.find(
|
||
(c) => c[0] === 'trails-path-bridleway-halo-satellite' && c[2] === 'visible'
|
||
);
|
||
assert.ok(trackHaloOn, 'halo для trails-track не включён');
|
||
assert.ok(pathHaloOn, 'halo для path/bridleway не включён');
|
||
|
||
env.calls.setLayoutProperty.length = 0;
|
||
env.mod.applyBaseLayer('schematic');
|
||
const trackHaloOff = env.calls.setLayoutProperty.find(
|
||
(c) => c[0] === 'trails-track-halo-satellite' && c[2] === 'none'
|
||
);
|
||
const pathHaloOff = env.calls.setLayoutProperty.find(
|
||
(c) => c[0] === 'trails-path-bridleway-halo-satellite' && c[2] === 'none'
|
||
);
|
||
assert.ok(trackHaloOff && pathHaloOff, 'halo не скрыт при возврате на схему');
|
||
});
|
||
|
||
// ── I-24: POI text-halo на спутнике становится чёрным ─────────────────
|
||
test('I-24: POI labels на спутнике получают чёрный halo и width=2', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
const haloColor = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'poi-labels' && c[1] === 'text-halo-color'
|
||
);
|
||
const haloWidth = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'poi-labels' && c[1] === 'text-halo-width'
|
||
);
|
||
assert.deepEqual(haloColor, ['poi-labels', 'text-halo-color', '#000000']);
|
||
assert.deepEqual(haloWidth, ['poi-labels', 'text-halo-width', 2]);
|
||
});
|
||
|
||
// ── I-25: POI text-halo на схеме возвращается к значениям из style.json ─
|
||
test('I-25: возврат на «Схему» восстанавливает POI halo из светлого style.json', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
env.calls.setPaintProperty.length = 0;
|
||
env.mod.applyBaseLayer('schematic');
|
||
const haloColor = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'poi-labels' && c[1] === 'text-halo-color'
|
||
);
|
||
assert.deepEqual(haloColor, ['poi-labels', 'text-halo-color', '#ffffff']);
|
||
});
|
||
|
||
test('I-25/dark: возврат на «Схему» в тёмной теме даёт halo из style-dark.json', () => {
|
||
const env = makeEnv({ themeDark: true });
|
||
env.mod.applyBaseLayer('satellite');
|
||
env.calls.setPaintProperty.length = 0;
|
||
env.mod.applyBaseLayer('schematic');
|
||
const haloColor = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'poi-labels' && c[1] === 'text-halo-color'
|
||
);
|
||
assert.deepEqual(haloColor, ['poi-labels', 'text-halo-color', '#1a1a2e']);
|
||
});
|
||
|
||
// ── background — единая satellite-константа #2a2a2a для обеих тем ─────
|
||
// (P1-4: ранее в спецификации был расходящийся набор констант, в т.ч.
|
||
// ошибочный #1a1a1a для светлой темы тёмнее, чем #2a2a2a для тёмной.
|
||
// ADR-004 §6 — одна константа #2a2a2a на обе темы.)
|
||
test('фон под спутником в светлой теме — единая константа #2a2a2a (P1-4)', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
const bg = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'background' && c[1] === 'background-color'
|
||
);
|
||
assert.deepEqual(bg, ['background', 'background-color', '#2a2a2a']);
|
||
});
|
||
|
||
test('фон под спутником в тёмной теме — та же константа #2a2a2a (P1-4)', () => {
|
||
const env = makeEnv({ themeDark: true });
|
||
env.mod.applyBaseLayer('satellite');
|
||
const bg = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'background' && c[1] === 'background-color'
|
||
);
|
||
assert.deepEqual(bg, ['background', 'background-color', '#2a2a2a']);
|
||
});
|
||
|
||
test('фон при возврате на «Схему» (light) — baseline #f0ede6 (Data §5)', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
env.calls.setPaintProperty.length = 0;
|
||
env.mod.applyBaseLayer('schematic');
|
||
const bg = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'background' && c[1] === 'background-color'
|
||
);
|
||
assert.deepEqual(bg, ['background', 'background-color', '#f0ede6']);
|
||
});
|
||
|
||
test('фон при возврате на «Схему» (dark) — baseline #1a1a2e, не #1a1a1a (P1-4 / P2-3)', () => {
|
||
const env = makeEnv({ themeDark: true });
|
||
env.mod.applyBaseLayer('satellite');
|
||
env.calls.setPaintProperty.length = 0;
|
||
env.mod.applyBaseLayer('schematic');
|
||
const bg = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'background' && c[1] === 'background-color'
|
||
);
|
||
assert.deepEqual(bg, ['background', 'background-color', '#1a1a2e']);
|
||
});
|
||
|
||
// ── P1-2: POI text-color синхронно с halo ─────────────────────────────
|
||
test('P1-2: на спутнике poi-labels text-color === #ffffff (читаемо поверх чёрного halo)', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
const textColor = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'poi-labels' && c[1] === 'text-color'
|
||
);
|
||
assert.deepEqual(textColor, ['poi-labels', 'text-color', '#ffffff']);
|
||
});
|
||
|
||
test('P1-2: возврат на «Схему» (light) восстанавливает poi-labels text-color === #333333', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
env.calls.setPaintProperty.length = 0;
|
||
env.mod.applyBaseLayer('schematic');
|
||
const textColor = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'poi-labels' && c[1] === 'text-color'
|
||
);
|
||
assert.deepEqual(textColor, ['poi-labels', 'text-color', '#333333']);
|
||
});
|
||
|
||
test('P1-2: возврат на «Схему» (dark) восстанавливает poi-labels text-color === #e0e0e0', () => {
|
||
const env = makeEnv({ themeDark: true });
|
||
env.mod.applyBaseLayer('satellite');
|
||
env.calls.setPaintProperty.length = 0;
|
||
env.mod.applyBaseLayer('schematic');
|
||
const textColor = env.calls.setPaintProperty.find(
|
||
(c) => c[0] === 'poi-labels' && c[1] === 'text-color'
|
||
);
|
||
assert.deepEqual(textColor, ['poi-labels', 'text-color', '#e0e0e0']);
|
||
});
|
||
|
||
// ── валидация входа onBaseLayerToggle() ───────────────────────────────
|
||
test('onBaseLayerToggle() игнорирует некорректное значение', () => {
|
||
const env = makeEnv();
|
||
env.mod.onBaseLayerToggle('hybrid');
|
||
assert.deepEqual(env.calls.setItem, []);
|
||
assert.deepEqual(env.calls.addSource, []);
|
||
});
|
||
|
||
// ── устойчивость: отсутствует window._map ─────────────────────────────
|
||
test('applyBaseLayer() без window._map не падает', () => {
|
||
const env = makeEnv({ noMap: true });
|
||
assert.doesNotThrow(() => env.mod.applyBaseLayer('satellite'));
|
||
});
|
||
|
||
// ── устойчивость: недоступный localStorage (private mode) ─────────────
|
||
test('getStoredBaseLayer() при недоступном localStorage возвращает "schematic"', () => {
|
||
const env = makeEnv({ noStorage: true });
|
||
assert.equal(env.mod.getStoredBaseLayer(), 'schematic');
|
||
});
|
||
|
||
test('onBaseLayerToggle() не падает при недоступном localStorage', () => {
|
||
const env = makeEnv({ noStorage: true });
|
||
assert.doesNotThrow(() => env.mod.onBaseLayerToggle('satellite'));
|
||
});
|
||
|
||
// ── z-order: спутник вставляется ПОД terrain/trails/POI ───────────────
|
||
test('Z-order: satellite-base вставляется beforeId=первый terrain/trails/poi слой', () => {
|
||
const env = makeEnv({
|
||
layers: ['background', 'osm-base', 'terrain-hillshade', 'trails-track', 'poi-circles'],
|
||
});
|
||
env.mod.applyBaseLayer('satellite');
|
||
const [, before] = env.calls.addLayer[0];
|
||
assert.equal(before, 'terrain-hillshade');
|
||
});
|
||
|
||
test('Z-order: если overlay-слоёв ещё нет — addLayer вызывается без beforeId', () => {
|
||
const env = makeEnv({ layers: ['background', 'osm-base'] });
|
||
env.mod.applyBaseLayer('satellite');
|
||
const [, before] = env.calls.addLayer[0];
|
||
assert.equal(before, undefined);
|
||
});
|
||
|
||
// ── повторный applyBaseLayer('satellite') не пересоздаёт source/layer ─
|
||
test('повторный applyBaseLayer("satellite") не дублирует addSource/addLayer', () => {
|
||
const env = makeEnv();
|
||
env.mod.applyBaseLayer('satellite');
|
||
env.mod.applyBaseLayer('schematic');
|
||
env.mod.applyBaseLayer('satellite');
|
||
assert.equal(env.calls.addSource.length, 1);
|
||
assert.equal(env.calls.addLayer.length, 1);
|
||
});
|