fix(ui): terrain-popup закрывается при открытии bottom-sheet (ET-014)
All checks were successful
All checks were successful
При открытии любого .bottom-sheet через openSheet() теперь принудительно скрывается #terrain-popup и снимается .active с #terrain-toggle. Это устраняет z-index конфликт (popup z=500 над sheet z=400) и убирает anti-pattern «два меню одновременно» на desktop без правки CSS-стека (marker-dialog z=500, search-panel, ruler-info — без регрессий). Реализация — Вариант A из ADR-019: helper closeTerrainPopup() + один вызов первой строкой в openSheet() после null-check. Для других sheets (sheet-route, sheet-recon, sheet-scenic, sheet-link, sheet-gpx) вызов безопасный no-op, REQ-F-06 выполняется автоматически. Тесты: - tests/unit/sheet_popup.test.js — 8 behavioral JS unit-тестов (TC-U-02, REQ-F-04, REQ-F-06 + ребра closeTerrainPopup). - tests/unit/test_sheet_popup.py — pytest-обёртка: статические проверки app.js (порядок вызовов в openSheet, маркеры блока), охранные тесты что z-stack не тронут и что gps_tracks.js/index.html не правились. Refs: ET-014 ADR: docs/work-items/ET-014/06-adr/ADR-019-terrain-popup-yields-to-sheet.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -203,9 +203,25 @@ function formatDist(m) {
|
|||||||
|
|
||||||
// ─── Sheet Management ──────────────────────────────────────────────
|
// ─── Sheet Management ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
// >>> ET-014 sheet-popup yield block (см. ADR-019)
|
||||||
|
// При открытии любого bottom-sheet'а принудительно закрываем
|
||||||
|
// #terrain-popup. Это устраняет z-index конфликт (popup z=500 над
|
||||||
|
// sheet z=400) и убирает anti-pattern «два меню открыты одновременно»
|
||||||
|
// на desktop. См. docs/work-items/ET-014/06-adr/ADR-019-*.
|
||||||
|
function closeTerrainPopup() {
|
||||||
|
const popup = document.getElementById('terrain-popup');
|
||||||
|
const btn = document.getElementById('terrain-toggle');
|
||||||
|
if (!popup || popup.style.display === 'none') return;
|
||||||
|
popup.style.display = 'none';
|
||||||
|
if (btn) btn.classList.remove('active');
|
||||||
|
document.removeEventListener('click', closeTerrainOnOutside);
|
||||||
|
}
|
||||||
|
|
||||||
function openSheet(id) {
|
function openSheet(id) {
|
||||||
const sheet = document.getElementById(id);
|
const sheet = document.getElementById(id);
|
||||||
if (!sheet) return;
|
if (!sheet) return;
|
||||||
|
// ET-014: terrain-popup yields to any opening sheet (ADR-019).
|
||||||
|
closeTerrainPopup();
|
||||||
// Close all other sheets first
|
// Close all other sheets first
|
||||||
document.querySelectorAll('.bottom-sheet.open').forEach(s => {
|
document.querySelectorAll('.bottom-sheet.open').forEach(s => {
|
||||||
if (s.id !== id) closeSheet(s.id);
|
if (s.id !== id) closeSheet(s.id);
|
||||||
@@ -214,6 +230,7 @@ function openSheet(id) {
|
|||||||
const backdrop = document.getElementById('sheet-backdrop');
|
const backdrop = document.getElementById('sheet-backdrop');
|
||||||
backdrop.classList.add('visible');
|
backdrop.classList.add('visible');
|
||||||
}
|
}
|
||||||
|
// <<< ET-014 sheet-popup yield block <<<
|
||||||
|
|
||||||
function closeSheet(id) {
|
function closeSheet(id) {
|
||||||
const sheet = document.getElementById(id);
|
const sheet = document.getElementById(id);
|
||||||
|
|||||||
259
tests/unit/sheet_popup.test.js
Normal file
259
tests/unit/sheet_popup.test.js
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ET-014 — поведенческие unit-тесты для closeTerrainPopup() и openSheet().
|
||||||
|
*
|
||||||
|
* Покрывают TC-U-01..TC-U-02 (часть) из docs/work-items/ET-014/04-test-plan.yaml,
|
||||||
|
* а также проверяют логику ADR-019: при открытии любого bottom-sheet
|
||||||
|
* `#terrain-popup` принудительно закрывается, а `#terrain-toggle` теряет
|
||||||
|
* класс `.active`. Поведение базируется на JS-функциях из блока ET-014 в
|
||||||
|
* src/web/app.js (между маркерами `// >>> ET-014 sheet-popup yield block`
|
||||||
|
* и `// <<< ET-014 sheet-popup yield block <<<`).
|
||||||
|
*
|
||||||
|
* Запуск: `node --test tests/unit/sheet_popup.test.js`
|
||||||
|
* (в CI оборачивается pytest-тестом tests/unit/test_sheet_popup.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-014-блок из app.js и собирает из него модуль, подставляя
|
||||||
|
* переданные зависимости (window, document, closeTerrainOnOutside,
|
||||||
|
* closeSheet). Стиль повторяет загрузчик ET-007 (base_layer.test.js).
|
||||||
|
*/
|
||||||
|
function loadEt014Module(deps) {
|
||||||
|
const src = fs.readFileSync(APP_JS, 'utf8');
|
||||||
|
const m = src.match(
|
||||||
|
/\/\/ >>> ET-014 sheet-popup yield block[^\n]*\n([\s\S]*?)\/\/ <<< ET-014 sheet-popup yield block/
|
||||||
|
);
|
||||||
|
assert.ok(m, 'ET-014-блок не найден в app.js (маркеры отсутствуют)');
|
||||||
|
const factory = new Function(
|
||||||
|
'window', 'document', 'closeTerrainOnOutside', 'closeSheet',
|
||||||
|
m[1] + '\nreturn { closeTerrainPopup, openSheet };'
|
||||||
|
);
|
||||||
|
return factory(
|
||||||
|
deps.window,
|
||||||
|
deps.document,
|
||||||
|
deps.closeTerrainOnOutside || (() => {}),
|
||||||
|
deps.closeSheet || (() => {}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Готовит мок-DOM: #terrain-popup, #terrain-toggle, #sheet-backdrop,
|
||||||
|
* а также произвольный набор bottom-sheets. Каждый bottom-sheet имеет
|
||||||
|
* classList с методами add/remove/contains и querySelectorAll-совместимый
|
||||||
|
* матчинг по селектору '.bottom-sheet.open' (через document.querySelectorAll).
|
||||||
|
*/
|
||||||
|
function makeEnv({
|
||||||
|
popupVisible = false,
|
||||||
|
toggleActive = false,
|
||||||
|
sheets = [], // [{ id, open }]
|
||||||
|
backdropVisible = false,
|
||||||
|
} = {}) {
|
||||||
|
const popup = {
|
||||||
|
style: { display: popupVisible ? 'block' : 'none' },
|
||||||
|
};
|
||||||
|
const _toggleClasses = new Set(['map-btn']);
|
||||||
|
if (toggleActive) _toggleClasses.add('active');
|
||||||
|
const toggle = {
|
||||||
|
classList: {
|
||||||
|
_classes: _toggleClasses,
|
||||||
|
add(c) { this._classes.add(c); },
|
||||||
|
remove(c) { this._classes.delete(c); },
|
||||||
|
contains(c) { return this._classes.has(c); },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const _backdropClasses = new Set();
|
||||||
|
if (backdropVisible) _backdropClasses.add('visible');
|
||||||
|
const backdrop = {
|
||||||
|
classList: {
|
||||||
|
_classes: _backdropClasses,
|
||||||
|
add(c) { this._classes.add(c); },
|
||||||
|
remove(c) { this._classes.delete(c); },
|
||||||
|
contains(c) { return this._classes.has(c); },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bottom-sheets с classList API.
|
||||||
|
const sheetEls = sheets.map(({ id, open }) => {
|
||||||
|
const _classes = new Set(['bottom-sheet']);
|
||||||
|
if (open) _classes.add('open');
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
classList: {
|
||||||
|
_classes,
|
||||||
|
add(c) { this._classes.add(c); },
|
||||||
|
remove(c) { this._classes.delete(c); },
|
||||||
|
contains(c) { return this._classes.has(c); },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const docCalls = {
|
||||||
|
removeEventListener: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const document = {
|
||||||
|
getElementById(id) {
|
||||||
|
if (id === 'terrain-popup') return popup;
|
||||||
|
if (id === 'terrain-toggle') return toggle;
|
||||||
|
if (id === 'sheet-backdrop') return backdrop;
|
||||||
|
const s = sheetEls.find((e) => e.id === id);
|
||||||
|
return s || null;
|
||||||
|
},
|
||||||
|
querySelectorAll(selector) {
|
||||||
|
if (selector === '.bottom-sheet.open') {
|
||||||
|
return sheetEls.filter((s) => s.classList.contains('open'));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
removeEventListener(type, fn) {
|
||||||
|
docCalls.removeEventListener.push([type, fn]);
|
||||||
|
},
|
||||||
|
addEventListener() { /* not used by closeTerrainPopup */ },
|
||||||
|
};
|
||||||
|
|
||||||
|
return { document, popup, toggle, backdrop, sheetEls, docCalls };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── TC-U-02 (часть А): popup закрывается при открытии sheet ────────────
|
||||||
|
test('TC-U-02: openSheet() закрывает открытый terrain-popup и снимает .active', () => {
|
||||||
|
const env = makeEnv({
|
||||||
|
popupVisible: true,
|
||||||
|
toggleActive: true,
|
||||||
|
sheets: [{ id: 'sheet-gps-filters', open: false }],
|
||||||
|
});
|
||||||
|
const mod = loadEt014Module({ document: env.document });
|
||||||
|
|
||||||
|
mod.openSheet('sheet-gps-filters');
|
||||||
|
|
||||||
|
assert.equal(env.popup.style.display, 'none', 'popup должен быть скрыт');
|
||||||
|
assert.ok(!env.toggle.classList.contains('active'),
|
||||||
|
'кнопка #terrain-toggle должна потерять класс active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── REQ-F-04 / AC-06: повторное открытие стабильно ─────────────────────
|
||||||
|
test('REQ-F-04: повторный openSheet() — sheet остаётся open, без артефактов', () => {
|
||||||
|
const env = makeEnv({
|
||||||
|
popupVisible: false,
|
||||||
|
sheets: [{ id: 'sheet-gps-filters', open: false }],
|
||||||
|
});
|
||||||
|
const mod = loadEt014Module({ document: env.document });
|
||||||
|
|
||||||
|
mod.openSheet('sheet-gps-filters');
|
||||||
|
const sheet = env.sheetEls.find((s) => s.id === 'sheet-gps-filters');
|
||||||
|
assert.ok(sheet.classList.contains('open'), 'sheet должен иметь класс open');
|
||||||
|
assert.ok(env.backdrop.classList.contains('visible'),
|
||||||
|
'backdrop должен быть видим');
|
||||||
|
|
||||||
|
// Повторный вызов — sheet остаётся открытым, никаких регрессий.
|
||||||
|
mod.openSheet('sheet-gps-filters');
|
||||||
|
assert.ok(sheet.classList.contains('open'), 'sheet всё ещё open');
|
||||||
|
assert.ok(env.backdrop.classList.contains('visible'),
|
||||||
|
'backdrop всё ещё visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── REQ-F-06: другие sheets — popup-helper тоже срабатывает (но no-op) ─
|
||||||
|
test('REQ-F-06: openSheet() для других sheets тоже зовёт closeTerrainPopup', () => {
|
||||||
|
// Popup закрыт изначально — closeTerrainPopup должна быть no-op.
|
||||||
|
const env = makeEnv({
|
||||||
|
popupVisible: false,
|
||||||
|
sheets: [
|
||||||
|
{ id: 'sheet-route', open: false },
|
||||||
|
{ id: 'sheet-recon', open: false },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const mod = loadEt014Module({ document: env.document });
|
||||||
|
|
||||||
|
mod.openSheet('sheet-route');
|
||||||
|
const sheet = env.sheetEls.find((s) => s.id === 'sheet-route');
|
||||||
|
assert.ok(sheet.classList.contains('open'));
|
||||||
|
assert.equal(env.popup.style.display, 'none', 'popup остаётся скрытым');
|
||||||
|
assert.ok(!env.toggle.classList.contains('active'),
|
||||||
|
'active не появляется (popup и не был открыт)');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── closeTerrainPopup — no-op если popup уже скрыт ─────────────────────
|
||||||
|
test('closeTerrainPopup: no-op если popup уже скрыт', () => {
|
||||||
|
const env = makeEnv({ popupVisible: false });
|
||||||
|
const mod = loadEt014Module({ document: env.document });
|
||||||
|
|
||||||
|
mod.closeTerrainPopup();
|
||||||
|
|
||||||
|
assert.equal(env.popup.style.display, 'none');
|
||||||
|
// removeEventListener не должен вызываться (нечего отписывать).
|
||||||
|
assert.equal(env.docCalls.removeEventListener.length, 0,
|
||||||
|
'removeEventListener не должен вызываться при закрытом popup');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── closeTerrainPopup: отписывает closeTerrainOnOutside ────────────────
|
||||||
|
test('closeTerrainPopup: при открытом popup отписывает click-listener', () => {
|
||||||
|
const env = makeEnv({ popupVisible: true, toggleActive: true });
|
||||||
|
const dummyHandler = function closeTerrainOnOutside() {};
|
||||||
|
const mod = loadEt014Module({
|
||||||
|
document: env.document,
|
||||||
|
closeTerrainOnOutside: dummyHandler,
|
||||||
|
});
|
||||||
|
|
||||||
|
mod.closeTerrainPopup();
|
||||||
|
|
||||||
|
assert.equal(env.popup.style.display, 'none');
|
||||||
|
assert.ok(!env.toggle.classList.contains('active'));
|
||||||
|
assert.equal(env.docCalls.removeEventListener.length, 1,
|
||||||
|
'removeEventListener должен быть вызван 1 раз');
|
||||||
|
assert.equal(env.docCalls.removeEventListener[0][0], 'click');
|
||||||
|
assert.equal(env.docCalls.removeEventListener[0][1], dummyHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── closeTerrainPopup: безопасен при отсутствии #terrain-popup ─────────
|
||||||
|
test('closeTerrainPopup: безопасен если #terrain-popup отсутствует', () => {
|
||||||
|
const env = makeEnv({ popupVisible: false });
|
||||||
|
// Перекроем getElementById чтобы вернуть null для terrain-popup.
|
||||||
|
const origGet = env.document.getElementById.bind(env.document);
|
||||||
|
env.document.getElementById = (id) => (id === 'terrain-popup' ? null : origGet(id));
|
||||||
|
const mod = loadEt014Module({ document: env.document });
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => mod.closeTerrainPopup());
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── openSheet: ранний выход если sheet не найден (без побочных эффектов) ─
|
||||||
|
test('openSheet: ранний выход если sheet не найден (popup не трогается)', () => {
|
||||||
|
const env = makeEnv({ popupVisible: true, toggleActive: true });
|
||||||
|
const mod = loadEt014Module({ document: env.document });
|
||||||
|
|
||||||
|
mod.openSheet('does-not-exist');
|
||||||
|
|
||||||
|
// popup остаётся открытым: helper вызывается ПОСЛЕ null-check на sheet.
|
||||||
|
assert.equal(env.popup.style.display, 'block',
|
||||||
|
'popup должен остаться открытым, если sheet не найден');
|
||||||
|
assert.ok(env.toggle.classList.contains('active'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── REQ-F-01: закрытие конкурирующих sheets продолжает работать ────────
|
||||||
|
test('openSheet: закрывает другие открытые sheets (через closeSheet)', () => {
|
||||||
|
const env = makeEnv({
|
||||||
|
sheets: [
|
||||||
|
{ id: 'sheet-route', open: true },
|
||||||
|
{ id: 'sheet-gps-filters', open: false },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const closeSheetCalls = [];
|
||||||
|
const mod = loadEt014Module({
|
||||||
|
document: env.document,
|
||||||
|
closeSheet: (id) => closeSheetCalls.push(id),
|
||||||
|
});
|
||||||
|
|
||||||
|
mod.openSheet('sheet-gps-filters');
|
||||||
|
|
||||||
|
assert.deepEqual(closeSheetCalls, ['sheet-route'],
|
||||||
|
'closeSheet должен быть вызван для sheet-route');
|
||||||
|
const target = env.sheetEls.find((s) => s.id === 'sheet-gps-filters');
|
||||||
|
assert.ok(target.classList.contains('open'));
|
||||||
|
});
|
||||||
195
tests/unit/test_sheet_popup.py
Normal file
195
tests/unit/test_sheet_popup.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
"""ET-014 — тесты sheet ⇄ terrain-popup взаимодействия (ADR-019).
|
||||||
|
|
||||||
|
ET-014 — исключительно фронтендовое изменение (см. ADR-019): правки
|
||||||
|
`src/web/app.js`. Никаких изменений в CSS, HTML, backend, миграциях.
|
||||||
|
В CI исполняется только `pytest tests/`, поэтому файл покрывает фичу
|
||||||
|
двумя способами:
|
||||||
|
|
||||||
|
1. Статические проверки структуры `src/web/app.js` — выполняются всегда.
|
||||||
|
2. Поведенческие JS unit-тесты (TC-U-02, REQ-F-04, REQ-F-06) —
|
||||||
|
запускаются через встроенный тест-раннер Node (`node --test`). Если
|
||||||
|
`node` в системе отсутствует — эта часть помечается `skip`.
|
||||||
|
|
||||||
|
Браузерные e2e-сценарии (TC-E-01..TC-E-06, TC-UI-01..TC-UI-08) требуют
|
||||||
|
Playwright-инфраструктуры, которой в репозитории нет. Их поведенческая
|
||||||
|
суть покрыта JS unit-тестами и статическими проверками ниже.
|
||||||
|
|
||||||
|
См.:
|
||||||
|
- ADR-019: docs/work-items/ET-014/06-adr/ADR-019-terrain-popup-yields-to-sheet.md
|
||||||
|
- TRZ: docs/work-items/ET-014/02-trz.md
|
||||||
|
- AC: docs/work-items/ET-014/03-acceptance-criteria.md
|
||||||
|
- Test plan: docs/work-items/ET-014/04-test-plan.yaml
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from shutil import which
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
APP_JS = REPO_ROOT / "src" / "web" / "app.js"
|
||||||
|
APP_CSS = REPO_ROOT / "src" / "web" / "app.css"
|
||||||
|
INDEX_HTML = REPO_ROOT / "src" / "web" / "index.html"
|
||||||
|
JS_TEST = REPO_ROOT / "tests" / "unit" / "sheet_popup.test.js"
|
||||||
|
|
||||||
|
|
||||||
|
def _read(path: Path) -> str:
|
||||||
|
assert path.is_file(), f"не найден {path}"
|
||||||
|
return path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Статические проверки app.js (ADR-019)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_app_js_has_et014_block_markers():
|
||||||
|
"""Блок ET-014 обрамлён маркерами для извлечения JS unit-тестами."""
|
||||||
|
js = _read(APP_JS)
|
||||||
|
assert "// >>> ET-014 sheet-popup yield block" in js, (
|
||||||
|
"нет открывающего маркера блока ET-014"
|
||||||
|
)
|
||||||
|
assert "// <<< ET-014 sheet-popup yield block <<<" in js, (
|
||||||
|
"нет закрывающего маркера блока ET-014"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_close_terrain_popup_function_defined():
|
||||||
|
"""ADR-019 §Решение/1: функция closeTerrainPopup() определена."""
|
||||||
|
js = _read(APP_JS)
|
||||||
|
assert "function closeTerrainPopup(" in js, (
|
||||||
|
"не определена функция closeTerrainPopup()"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_close_terrain_popup_inside_block():
|
||||||
|
"""closeTerrainPopup() расположена внутри ET-014-блока (для unit-тестов)."""
|
||||||
|
js = _read(APP_JS)
|
||||||
|
block_start = js.index("// >>> ET-014 sheet-popup yield block")
|
||||||
|
block_end = js.index("// <<< ET-014 sheet-popup yield block <<<")
|
||||||
|
block = js[block_start:block_end]
|
||||||
|
assert "function closeTerrainPopup(" in block, (
|
||||||
|
"closeTerrainPopup() должна быть внутри ET-014-блока"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_sheet_calls_close_terrain_popup_first():
|
||||||
|
"""ADR-019 §Решение/2: closeTerrainPopup() — первый вызов в openSheet()
|
||||||
|
после null-check на sheet."""
|
||||||
|
js = _read(APP_JS)
|
||||||
|
# Берём тело openSheet до первой закрывающей фигурной скобки на новой строке.
|
||||||
|
m = re.search(
|
||||||
|
r"function openSheet\(id\)\s*\{([\s\S]*?)\n\}",
|
||||||
|
js,
|
||||||
|
)
|
||||||
|
assert m, "функция openSheet(id) не найдена"
|
||||||
|
body = m.group(1)
|
||||||
|
# Проверим порядок: null-check, потом closeTerrainPopup, потом всё остальное.
|
||||||
|
nullcheck_pos = body.find("if (!sheet) return;")
|
||||||
|
close_popup_pos = body.find("closeTerrainPopup()")
|
||||||
|
close_sheet_pos = body.find("closeSheet(")
|
||||||
|
add_open_pos = body.find("classList.add('open')")
|
||||||
|
|
||||||
|
assert nullcheck_pos >= 0, "null-check на sheet в openSheet() отсутствует"
|
||||||
|
assert close_popup_pos > nullcheck_pos, (
|
||||||
|
"closeTerrainPopup() должна вызываться ПОСЛЕ null-check"
|
||||||
|
)
|
||||||
|
assert close_sheet_pos > close_popup_pos, (
|
||||||
|
"closeTerrainPopup() должна вызываться ДО закрытия других sheets"
|
||||||
|
)
|
||||||
|
assert add_open_pos > close_popup_pos, (
|
||||||
|
"closeTerrainPopup() должна вызываться ДО classList.add('open')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_sheet_calls_close_terrain_popup_exactly_once():
|
||||||
|
"""REQ-NF-02: никакого дублирования вызовов (не должно быть лишних
|
||||||
|
обработчиков). closeTerrainPopup() вызывается ровно один раз в openSheet."""
|
||||||
|
js = _read(APP_JS)
|
||||||
|
m = re.search(
|
||||||
|
r"function openSheet\(id\)\s*\{([\s\S]*?)\n\}",
|
||||||
|
js,
|
||||||
|
)
|
||||||
|
assert m, "функция openSheet(id) не найдена"
|
||||||
|
body = m.group(1)
|
||||||
|
calls = body.count("closeTerrainPopup()")
|
||||||
|
assert calls == 1, (
|
||||||
|
f"closeTerrainPopup() должна вызываться ровно один раз в openSheet(), "
|
||||||
|
f"найдено {calls}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Статические проверки: что НЕ меняется (ADR-019 §Что НЕ меняется)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_z_index_stack_unchanged_for_affected_widgets():
|
||||||
|
"""ADR-019 §Что НЕ меняется: z-index ключевых виджетов из конфликта
|
||||||
|
(.bottom-sheet, #sheet-backdrop, .terrain-popup, #marker-dialog)
|
||||||
|
остаётся неизменным. Эти значения — фундамент аргументации ADR-019
|
||||||
|
(Вариант A не правит CSS), любая их правка ломает обоснование.
|
||||||
|
|
||||||
|
REQ-NF-03: marker-dialog (z=500) сохраняется на верху относительно sheet'ов.
|
||||||
|
"""
|
||||||
|
css = _read(APP_CSS)
|
||||||
|
expected = [
|
||||||
|
(".bottom-sheet", "z-index: 400"),
|
||||||
|
("#sheet-backdrop", "z-index: 390"),
|
||||||
|
("#marker-dialog", "z-index: 500"),
|
||||||
|
(".terrain-popup", "z-index: 500"),
|
||||||
|
]
|
||||||
|
for selector, z in expected:
|
||||||
|
sel_pos = css.find(selector)
|
||||||
|
assert sel_pos >= 0, f"селектор {selector} не найден в app.css"
|
||||||
|
# Смотрим в окне 600 символов после селектора (CSS-блок укладывается).
|
||||||
|
window = css[sel_pos:sel_pos + 600]
|
||||||
|
assert z in window, (
|
||||||
|
f"в блоке {selector} отсутствует {z}; ADR-019 запрещает менять z-stack"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_gps_tracks_js_not_touched_by_et014():
|
||||||
|
"""ADR-019 §Что НЕ меняется: src/web/gps_tracks.js не правится ET-014."""
|
||||||
|
gps = _read(REPO_ROOT / "src" / "web" / "gps_tracks.js")
|
||||||
|
# Маркеров ET-014 в gps_tracks.js не должно быть — логика живёт в openSheet.
|
||||||
|
assert "ET-014" not in gps, (
|
||||||
|
"ET-014 не должен изменять src/web/gps_tracks.js (см. ADR-019)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_html_not_touched_by_et014():
|
||||||
|
"""ADR-019 §Что НЕ меняется: src/web/index.html без изменений."""
|
||||||
|
html = _read(INDEX_HTML)
|
||||||
|
assert "ET-014" not in html, (
|
||||||
|
"ET-014 не должен изменять src/web/index.html (см. ADR-019)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Поведенческие JS unit-тесты через Node (TC-U-02, REQ-F-04, REQ-F-06)
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
node_required = pytest.mark.skipif(
|
||||||
|
which("node") is None,
|
||||||
|
reason="node не установлен — поведенческие JS unit-тесты пропущены",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@node_required
|
||||||
|
def test_js_unit_tests_pass():
|
||||||
|
"""TC-U-02 / REQ-F-04 / REQ-F-06: behavioral JS-тесты через `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-тесты ET-014 упали (код {result.returncode}):\n"
|
||||||
|
f"{result.stdout}\n{result.stderr}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user