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 ──────────────────────────────────────────────
|
||||
|
||||
// >>> 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) {
|
||||
const sheet = document.getElementById(id);
|
||||
if (!sheet) return;
|
||||
// ET-014: terrain-popup yields to any opening sheet (ADR-019).
|
||||
closeTerrainPopup();
|
||||
// Close all other sheets first
|
||||
document.querySelectorAll('.bottom-sheet.open').forEach(s => {
|
||||
if (s.id !== id) closeSheet(s.id);
|
||||
@@ -214,6 +230,7 @@ function openSheet(id) {
|
||||
const backdrop = document.getElementById('sheet-backdrop');
|
||||
backdrop.classList.add('visible');
|
||||
}
|
||||
// <<< ET-014 sheet-popup yield block <<<
|
||||
|
||||
function closeSheet(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