analyst(ET): auto-commit from analyst run_id=25
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 6s
CI / build (push) Successful in 1s

This commit is contained in:
2026-05-31 17:13:21 +00:00
parent 578e77833f
commit 9c81ebd653
14 changed files with 2549 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
# Business Request: ET-005: Спутниковая карта (Схема/Спутник)
Work Item ID: ET-007
## Description
TBD

View File

@@ -0,0 +1,80 @@
---
type: business-request
work_item_id: ET-008
title: "Smoke test analyst integration"
created_at: 2026-05-31
source: pipeline-smoke
requester: claude-bot
synthetic: true
---
# Бизнес-запрос — ET-008 (Smoke test analyst integration)
## Контекст
Это **smoke-работа** для проверки интеграции аналитика в пайплайн
`analyst → architect → coder → tester`. Реального заказчика нет;
запрос синтезирован, чтобы проверить, что:
1. Аналитик умеет создать полный пакет артефактов
(BRD / TRZ / AC / test-plan / UI test cases) без ручного вмешательства.
2. Архитектор может декомпозировать ТЗ в исполнимый план.
3. Кодер может реализовать минимальное изменение по плану.
4. Тестировщик может прогнать функциональные и Playwright-тесты,
и автоматически закрыть задачу.
В отличие от ET-007 (dry-run по реальной фиче), ET-008 — это **smoke**:
скоуп ещё уже, никакого нового UX-функционала для пользователя нет,
только технический маркер, видимый Playwright-у.
Требования к синтетическому скоупу (жёстче, чем у ET-007):
- Изменение исключительно во фронтенде (`src/web/index.html`,
`src/web/app.css`). JavaScript **не трогаем**.
- Не ломает существующий функционал: карта, темы, роутинг, GPX, рельеф
(hillshade/TRI), POI, разведка, линейка, единицы измерения, поиск,
переключатели слоёв.
- Виден Playwright-у при специальном условии (`?smoke=et-008` в URL),
но **невидим** обычному пользователю при чистой загрузке.
- Тривиально откатывается: одна `<div>` в `index.html` + один CSS-блок.
- Не зависит от сети, БД, времени, пользовательских действий.
## Исходная формулировка
> Нужен невидимый по умолчанию DOM-маркер, который сигнализирует, что
> текущая сборка прошла полный конвейер аналитик → архитектор → кодер →
> тестировщик. Маркер должен появляться в углу карты только когда в
> URL присутствует параметр `?smoke=et-008` (или хеш `#smoke=et-008`).
> Это нужно автоматическим тестам пайплайна — они проверяют, что
> deploy на test содержит сборку правильного work-item, и что фронтенд
> отвечает на разметку без падений.
## Уточнения (приняты по умолчанию для smoke)
1. Видимый идентификатор маркера: `#pipeline-smoke`.
2. Текст маркера (на русском): «ET-008 ✓».
3. Позиция: левый нижний угол экрана (не конфликтует с
`#map-controls-r` справа, с `bottom-sheet` снизу-по центру, с
`.maplibregl-ctrl-attrib` справа-внизу). Конкретно — `left: 8px;
bottom: 8px`.
4. Размер: 11px шрифт, нижний регистр, полупрозрачный фон, нейтральный
тёмно-серый цвет; не должен закрывать ничего важного даже если
как-то стал видимым случайно.
5. Условие отображения: маркер присутствует в DOM **всегда**, но имеет
`display: none` по умолчанию. Видим становится, когда у `<body>`
есть класс `smoke-mode`. Класс ставится автоматически инлайн-скриптом
в `<head>`, если `location.search` содержит `smoke=et-008` ИЛИ
`location.hash` содержит `smoke=et-008`.
6. Не использовать `localStorage`. Маркер ничего не сохраняет —
только реагирует на URL текущего хита.
7. Доступность: `aria-hidden="true"`, `role="presentation"` — маркер
технический, не должен попадать в screen reader.
8. Семантика body-класса: имя `smoke-mode` зарезервировано **только**
за этим work item. Если в будущем понадобятся аналогичные маркеры
для других work items — расширяем семантику тем же классом, но с
дополнительными data-атрибутами.
9. Backend / БД / тайл-эндпоинты / OSRM / app.js / units.js / gpx.js
**не затрагиваются**.
10. Тёмная и светлая темы: маркер использует одинаковые цвета в обеих
темах (тёмный фон + светлый текст), читаемость гарантируется
собственными цветами, а не наследованием от темы.

View File

@@ -0,0 +1,120 @@
---
type: brd
work_item_id: ET-008
title: "BRD: Smoke test analyst integration"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
---
# BRD — ET-008: Smoke test analyst integration
## 1. Цель
Подтвердить, что цепочка агентов `analyst → architect → coder → tester`
работоспособна **end-to-end**: аналитик выдаёт валидные артефакты,
архитектор по ним строит план, кодер минимально его реализует,
тестировщик прогоняет автотесты (включая Playwright) и автоматически
закрывает задачу.
Полезной фичи для конечного пользователя нет — это намеренно
технический smoke. Минимальное изменение во фронтенде нужно лишь как
«отпечаток сборки», который Playwright увидит и подтвердит.
## 2. Контекст
- Веб-приложение: MapLibre GL JS 4.7 + vanilla JS, без фреймворка.
- Базовая страница: `src/web/index.html`; стили — `src/web/app.css`;
логика — `src/web/app.js` (НЕ ТРОГАТЬ в рамках ET-008).
- ET-007 (Спутниковая карта) уже использовал такую же idea-pipeline
для dry-run; ET-008 — следующая итерация, ещё минимальнее: ноль JS,
ноль внешних зависимостей, ноль localStorage.
- Имеется работающая Playwright-инфра (`tests/web/e2e/`), которая
умеет открывать тест-окружение и снимать скриншоты.
## 3. Scope
### In scope
| # | Функция |
|------|------------------------------------------------------------------------------------------------------|
| F-01 | DOM-элемент `#pipeline-smoke` с текстом «ET-008 ✓» в `src/web/index.html` |
| F-02 | CSS-правила для `#pipeline-smoke` в `src/web/app.css` (скрыт по умолчанию) |
| F-03 | CSS-правила для `body.smoke-mode #pipeline-smoke` (видим в smoke-режиме) |
| F-04 | Инлайн-скрипт в `<head>` `src/web/index.html`, добавляющий `smoke-mode` к `<body>` по URL-условию |
| F-05 | Условие активации: `location.search.includes('smoke=et-008')` ИЛИ `location.hash.includes('smoke=et-008')` |
| F-06 | По умолчанию (без параметра) маркер физически в DOM, но `display:none` |
| F-07 | Маркер `aria-hidden="true"` / `role="presentation"` — не попадает в screen reader |
| F-08 | Позиционирование: `position: fixed; left: 8px; bottom: 8px; z-index` ниже sheets/popup'ов |
| F-09 | Совместимость с тёмной и светлой темами — собственные цвета, не наследует темовые переменные |
### Out of scope
- JavaScript-логика в `src/web/app.js`, `units.js`, `gpx.js`.
- Любой backend / БД / OSRM / тайлы.
- Сохранение состояния (localStorage / sessionStorage / cookies).
- Видимость по нажатию кнопки / shortcut'у клавиатуры / тапу — только URL.
- Локализация (текст «ET-008 ✓» одинаков для всех языков).
- Анимации появления / скрытия.
- Управление через query API (REST/IPC).
- Мобильный layout-tuning (маркер одинаков на desktop и mobile).
- Любые другие work item identifiers, кроме `et-008`.
## 4. Метрики успеха
| Метрика | Критерий |
|-----------------------------------------------|-----------------------------------------------------------------------------------------|
| Наличие маркера в DOM | На любой загрузке `document.querySelector('#pipeline-smoke')` не null |
| Скрытость по умолчанию | На `?` (без параметра) `getComputedStyle(...).display === 'none'` |
| Видимость в smoke-режиме | На `?smoke=et-008` или `#smoke=et-008` маркер визуально различим в левом нижнем углу |
| Контент маркера | `textContent.trim() === 'ET-008 ✓'` |
| Корректность ARIA | `aria-hidden="true"`, `role="presentation"` |
| Отсутствие конфликтов | Маркер не перекрывает `#map-controls-r`, sheets, `.maplibregl-ctrl-attrib` |
| Стабильность тем | После `toggleTheme()` маркер остаётся видим/скрыт согласно своему правилу |
| Отсутствие регрессий | Все существующие E2E (ET-001..ET-007) остаются зелёными |
| Время от клика до отображения | После загрузки страницы маркер показан **до** `DOMContentLoaded` end (инлайн-скрипт) |
| Отсутствие сетевых запросов | ET-008 не порождает ни одного нового HTTP-запроса |
| Откатываемость | Полный откат — 3 диффа (HTML head + HTML body + CSS блок), 1 коммит |
## 5. Риски
| Риск | Вероятность | Влияние | Митигация |
|---------------------------------------------------------------------|-------------|---------|------------------------------------------------------------------------------------|
| Инлайн-скрипт в `<head>` ломает CSP | Низкая | Среднее | CSP не настроен сейчас. Если когда-то будет — переключить на data-атрибут + CSS-only|
| `#pipeline-smoke` перекрывает важный UI (например `.maplibregl-ctrl-scale`) | Низкая | Низкое | `z-index: 1` — ниже всех плавающих контролов; позиция фиксирована подальше |
| Маркер случайно засветился пользователю (например shared link с `?smoke=et-008`) | Низкая | Низкое | Текст нейтральный, не раскрывает ничего внутреннего; стиль ненавязчивый |
| Body-класс `smoke-mode` конфликтует с будущим классом темы | Низкая | Низкое | Префикс `smoke-` зарезервирован; в проекте сейчас нет похожих имён |
| ARIA-атрибуты «protect» сломаются при mutation observer на body | Низкая | Низкое | `aria-hidden` ставится статически в HTML, не из JS |
| Маркер ломает Playwright-снимки других тестов | Низкая | Низкое | По умолчанию `display:none` — на скриншоте не виден, BB-rect = 0 |
| Лишний div снижает производительность DOM | Очень низкая| Очень низкое| 1 элемент, без подписок, без обработчиков |
| Старые браузеры не поддерживают `includes` на строках | Очень низкая| Низкое | `String.prototype.includes` — ES2015, поддержано во всех целевых браузерах |
## 6. Зависимости
- **Внешние сервисы**: нет.
- **Внутренние**: только `src/web/index.html`, `src/web/app.css`.
- Не зависит от ET-005 / ET-006 / ET-007.
- Не блокирует ни одну фазу (PH-1..PH-9).
- Не вносит изменений в `app.js`, `units.js`, `gpx.js`.
- Не меняет `style.json`, `style-dark.json`.
- Backend, БД, OSRM, миграции, контейнеры — не затрагиваются.
- CI/CD — не меняется. Только новые E2E-тесты для самого маркера в
`tests/web/e2e/pipeline-smoke.spec.ts`.
## 7. Критерии smoke-успеха (для пайплайна)
Эта работа считается завершённой, когда:
1. Все 5 артефактов аналитика (`00`..`04b`) присутствуют и валидны.
2. Архитектор выдаёт минимум один план в `05-architecture.md` или
эквивалент.
3. Кодер вносит изменения только в `src/web/index.html` +
`src/web/app.css` (по diff'у видно).
4. Все тесты UT/IT/E2E зелёные на test-окружении.
5. Tester автоматически закрывает задачу.
Само наличие зелёного smoke-прогона важнее, чем визуальная
эстетика маркера.

View File

@@ -0,0 +1,281 @@
---
type: trz
work_item_id: ET-008
title: "ТЗ: Smoke test analyst integration"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
depends_on:
- "ET-008/01-brd.md"
---
# Техническое задание — ET-008: Smoke test analyst integration
## 1. Общая архитектура изменений
Изменение строго фронтовое и максимально минимальное. Затрагиваются
**ровно два файла**:
- `src/web/index.html`
- В `<head>` добавить инлайн-скрипт, который выставляет
`document.body.classList.add('smoke-mode')`, если в URL присутствует
`smoke=et-008``search` или `hash`).
- В `<body>` (рядом с `<div id="map">` или сразу перед `<!-- Scripts -->`)
добавить элемент `<div id="pipeline-smoke" aria-hidden="true"
role="presentation">ET-008 ✓</div>`.
- `src/web/app.css`
- В конец файла добавить блок с правилами для `#pipeline-smoke`.
**Запрещено** трогать: `src/web/app.js`, `src/web/units.js`,
`src/web/gpx.js`, `src/web/style.json`, `src/web/style-dark.json`,
`src/api/**`, `tests/api/**`, миграции, Docker-конфигурацию.
## 2. Функциональные требования
### REQ-F-01. DOM-маркер `#pipeline-smoke`
В `src/web/index.html` добавить элемент:
```html
<div id="pipeline-smoke" aria-hidden="true" role="presentation">ET-008 ✓</div>
```
Размещение: после `<div id="map"></div>` (т.е. внутри `<body>`,
сразу после карты), но **до** `<!-- Scripts -->`. Конкретное место —
после блока `<div id="no-data-warning">` или эквивалентного места,
где сейчас лежат «свободно плавающие» элементы (`#ruler-toast`,
`#app-toast`).
Текст узла: ровно `ET-008 ✓` (без кавычек, с одним пробелом перед
галочкой, U+2713 CHECK MARK).
### REQ-F-02. Инлайн-скрипт активации в `<head>`
В `<head>` `src/web/index.html`, **после** `<link rel="stylesheet" href="app.css">`
и **до** закрывающего `</head>`, добавить:
```html
<script>
// ET-008: smoke marker activation. Pure URL-based, no storage, no I/O.
(function () {
try {
var s = (location.search || '') + ' ' + (location.hash || '');
if (s.indexOf('smoke=et-008') !== -1) {
document.documentElement.classList.add('smoke-mode');
}
} catch (e) { /* swallow — smoke is non-critical */ }
})();
</script>
```
Замечания:
- Скрипт ставит класс на `<html>` (`documentElement`), а не на
`<body>`, потому что на момент выполнения (в `<head>`) `<body>`
ещё не существует. Эквивалентный селектор для CSS —
`html.smoke-mode #pipeline-smoke`. Это требование к CSS-блоку
(см. REQ-F-04).
- `try/catch` обязателен — если по какой-то причине `location`
не доступен (тест с jsdom без URL), скрипт не должен ронять
страницу.
- Скрипт **синхронный**, не модуль, без `defer`/`async` — он
должен сработать до того, как CSS попытается применить
`display:none` к видимому в smoke маркеру (избежать FOUC).
### REQ-F-03. CSS базовое состояние (скрыт)
В конец `src/web/app.css` добавить:
```css
/* ── ET-008: pipeline smoke marker ───────────────────────────
По умолчанию полностью скрыт. Включается классом .smoke-mode
на <html>, который ставит инлайн-скрипт в <head>, если
в URL присутствует ?smoke=et-008 или #smoke=et-008.
──────────────────────────────────────────────────────────── */
#pipeline-smoke {
display: none;
}
```
### REQ-F-04. CSS активное состояние (видим)
Сразу после REQ-F-03 в `src/web/app.css`:
```css
html.smoke-mode #pipeline-smoke {
display: inline-block;
position: fixed;
left: 8px;
bottom: 8px;
z-index: 1;
padding: 2px 6px;
font: 11px/1.4 system-ui, -apple-system, sans-serif;
color: #e7e7e7;
background: rgba(20, 20, 28, 0.7);
border-radius: 4px;
pointer-events: none;
user-select: none;
letter-spacing: 0.02em;
}
```
Значения цвета и фона **не зависят** от темы — это намеренно, чтобы
маркер был стабильно читаем и в светлой, и в тёмной теме без отдельных
правил `body.theme-dark`.
### REQ-F-05. Условие активации
Маркер показывается, если **любое** из условий выполнено:
- `location.search` (часть URL после `?`) содержит подстроку
`smoke=et-008`;
- `location.hash` (часть URL после `#`) содержит подстроку
`smoke=et-008`.
Сравнение **точное**, чувствительно к регистру. Любое отклонение
(`smoke=ET-008`, `smoke=et_008`, `smoke=et-007`, `smoketest=et-008`)
**не** активирует маркер.
### REQ-F-06. Не-конфликтность с темами
После `toggleTheme()` маркер:
- если был видим (smoke-mode) — остаётся видимым с теми же цветами;
- если был скрыт (no smoke-mode) — остаётся скрытым.
То есть body-класс `theme-dark` не влияет на отображение
`#pipeline-smoke`.
### REQ-F-07. Не-конфликтность с layout
`z-index: 1` ниже, чем у `#map-controls-r` (фактический z-index у
существующих контролов — выше). Маркер не должен:
- перекрывать `#map-controls-r` (top-right);
- перекрывать `.maplibregl-ctrl-attrib` (bottom-right);
- мешать `#toolbar` на мобильном (нижняя панель);
- перекрывать любой `.bottom-sheet`, когда тот открыт.
В частности на мобильном бутылочное горлышко — `#toolbar` снизу.
Маркер в `left: 8px; bottom: 8px` помещается слева, тулбар обычно
центрирован/растянут, так что небольшой риск перекрытия допустим,
но визуально не критичен (маркер маленький и полупрозрачный).
### REQ-F-08. ARIA
- `aria-hidden="true"` — обязательно.
- `role="presentation"` — обязательно.
- Никаких подсказок (`title`), `aria-label`, фокусируемых элементов
внутри.
### REQ-F-09. Отсутствие сетевых эффектов
Реализация не должна:
- добавлять `<link>` / `<script src="...">` / `<img>` / `<iframe>`;
- обращаться к `fetch` / `XMLHttpRequest` / `navigator.sendBeacon`;
- использовать `localStorage` / `sessionStorage` / `cookie` /
`IndexedDB` / `caches`.
### REQ-F-10. Совместимость с Playwright `route(...)`
Playwright-тесты ET-008 имеют право ассертить отсутствие новых
сетевых запросов после добавления маркера (например, через
`page.on('request', ...)`). Любая сетевая активность из ET-008 —
дефект.
## 3. Нефункциональные требования
### REQ-NF-01. Производительность
- Дополнительный DOM-узел — 1 (`<div>`).
- Дополнительный CSS-блок — ≤ 20 строк.
- Дополнительный JS — инлайн ≤ 10 строк, выполняется **один раз**
до парсинга `<body>`.
- Никаких подписок на события, MutationObserver, requestAnimationFrame.
### REQ-NF-02. Совместимость браузеров
Те же, что у основного приложения:
- Chrome ≥ 110, Firefox ≥ 110, Safari ≥ 16, мобильные Chrome/Safari
iOS ≥ 16.
- `String.prototype.indexOf` — ES1, поддержано всегда.
### REQ-NF-03. Логирование
В нормальной работе скрипт ничего не пишет в консоль.
В исключительной — молча проглатывает (`try/catch` без `console.warn`).
Это смягчение для тестов, которые ассертят «no console.error».
### REQ-NF-04. Локализация
Текст `ET-008 ✓` не локализуется. Это технический идентификатор,
одинаковый во всех языках.
### REQ-NF-05. Безопасность
- Никаких пользовательских данных в маркере.
- Никакого `innerHTML` / `eval` / `Function(...)`.
- Никаких внешних URL.
## 4. Структура данных
### REQ-D-01. URL-параметры
| Параметр | Тип | Значение | Эффект |
|-----------------------|----------|---------------------------|-----------------------------------------|
| `?smoke=et-008` | query | строго `et-008` | `html.classList.add('smoke-mode')` |
| `#smoke=et-008` | fragment | строго `et-008` | то же самое |
| (нет параметра) | — | — | маркер `display:none` |
| `?smoke=...` иное | query | любое отличное от `et-008`| маркер `display:none` |
| `#smoke=...` иное | fragment | любое отличное от `et-008`| маркер `display:none` |
### REQ-D-02. Классы CSS
| Класс | Селектор где | Назначение |
|----------------------|-------------------------------|-------------------------------------|
| `smoke-mode` | на `<html>` | флаг видимости маркера |
## 5. UI / UX контракт
| Элемент | Местоположение | Поведение |
|---------------------|-----------------------------------------------------------------|------------------------------------------------------------------------|
| `#pipeline-smoke` | `<body>`, после `#map`, до `<!-- Scripts -->` | По умолчанию `display:none`. В smoke-режиме — fixed left:8 bottom:8 |
| Содержимое | Текстовый узел `ET-008 ✓` | Не меняется в рантайме |
| Фон | `rgba(20,20,28,0.7)` | Полупрозрачный, чтобы видеть карту под ним |
| Текст | `#e7e7e7`, 11px | Контраст ≥ 4.5:1 над тёмным фоном маркера |
| Курсор / фокус | `pointer-events: none`, не фокусируется | Не мешает кликам по карте под ним |
## 6. Тестируемость
- **Unit**: проверка наличия CSS-блока и DOM-узла через парсинг файлов
(без запуска браузера).
- **Integration**: запуск страницы в jsdom с поддельным `location`,
проверка добавления класса.
- **E2E (Playwright)**: загрузка test-окружения с `?smoke=et-008`
и без, проверка видимости/скрытости, отсутствия сетевых запросов,
отсутствия console.error.
- **UI (Playwright скриншоты)**: см. `04b-ui-test-cases.md`.
## 7. Совместимость и миграции
- БД: миграций нет.
- API: новых эндпоинтов нет.
- Конфиги: новых ENV не добавляем.
- Существующие work item: не затрагиваются (только статические файлы,
никаких JS-символов).
- CI: новые тесты добавляются как файл
`tests/web/e2e/pipeline-smoke.spec.ts`, существующая команда
`make test` подхватывает их без изменения конфигов.
## 8. Откат
В случае проблем:
1. В `src/web/index.html` удалить инлайн-`<script>` из `<head>`
(REQ-F-02).
2. В `src/web/index.html` удалить `<div id="pipeline-smoke">…</div>`
из `<body>` (REQ-F-01).
3. В `src/web/app.css` удалить блок с правилами для `#pipeline-smoke`
(REQ-F-03 и REQ-F-04).
Полный откат — один коммит, без побочных эффектов и без изменения
JS / API / БД.

View File

@@ -0,0 +1,177 @@
---
type: acceptance-criteria
work_item_id: ET-008
title: "AC: Smoke test analyst integration"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
depends_on:
- "ET-008/01-brd.md"
- "ET-008/02-trz.md"
---
# Критерии приёмки — ET-008: Smoke test analyst integration
Формат: Gherkin. Сценарии — в браузере приложения, на test-окружении
`https://openclaw.mva154.duckdns.org/enduro/`, если не указано иное.
## AC-01. Маркер присутствует в DOM при любой загрузке
```
Given пользователь открывает приложение в браузере без query-параметров
When страница полностью загружена
Then в DOM существует элемент с id="pipeline-smoke"
And его textContent.trim() == "ET-008 ✓"
And у элемента aria-hidden="true"
And у элемента role="presentation"
```
## AC-02. Маркер скрыт по умолчанию
```
Given пользователь открывает приложение без параметра smoke
When страница загружена
Then getComputedStyle(#pipeline-smoke).display == "none"
And элемент не виден визуально (boundingClientRect.height == 0)
```
## AC-03. Маркер виден при ?smoke=et-008 в query
```
Given пользователь открывает приложение с URL "...?smoke=et-008"
When страница загружена
Then у элемента <html> присутствует класс "smoke-mode"
And getComputedStyle(#pipeline-smoke).display == "inline-block"
And маркер визуально расположен в левом нижнем углу
(left ≈ 8px, bottom ≈ 8px от края viewport)
```
## AC-04. Маркер виден при #smoke=et-008 в hash
```
Given пользователь открывает приложение с URL "...#smoke=et-008"
When страница загружена
Then у элемента <html> присутствует класс "smoke-mode"
And маркер визуально виден в левом нижнем углу
```
## AC-05. Маркер скрыт при неверном значении параметра
```
Given пользователь открывает приложение с URL "...?smoke=et-007"
When страница загружена
Then у элемента <html> класса "smoke-mode" НЕТ
And getComputedStyle(#pipeline-smoke).display == "none"
```
## AC-06. Маркер скрыт при неверном имени параметра
```
Given пользователь открывает приложение с URL "...?smoketest=et-008"
When страница загружена
Then у элемента <html> класса "smoke-mode" НЕТ
And маркер не виден
```
## AC-07. Корректность ARIA
```
Given маркер видим (?smoke=et-008)
When ассистивная технология сканирует страницу
Then узел #pipeline-smoke не попадает в accessibility tree
(aria-hidden="true")
And screen reader не озвучивает "ET-008 ✓"
```
## AC-08. Совместимость с переключением темы
```
Given маркер видим (?smoke=et-008) в светлой теме
When пользователь нажимает #btn-theme (включение тёмной темы)
Then тема приложения меняется (body.theme-dark)
And маркер #pipeline-smoke остаётся видимым
And его background и color не изменились
And обратное переключение темы не скрывает маркер
```
## AC-09. Маркер не перекрывает важные элементы UI
```
Given маркер видим
When пользователь смотрит на интерфейс
Then маркер не перекрывает #map-controls-r
And не перекрывает .maplibregl-ctrl-attrib
And при открытии bottom-sheet маркер либо остаётся под sheet,
либо вообще исчезает за ним (sheet z-index выше)
And клики «сквозь» маркер по карте проходят
(pointer-events: none на маркере)
```
## AC-10. Отсутствие сетевых запросов от ET-008
```
Given Playwright перехватывает все запросы
When страница загружена с ?smoke=et-008
Then ни одного НОВОГО HTTP-запроса не порождено логикой ET-008
(только базовые тайлы, app.js, units.js, gpx.js, app.css)
And в частности нет запросов к /api/smoke/* или подобному
```
## AC-11. Отсутствие console.error
```
Given включён сбор console-сообщений
When страница загружена с ?smoke=et-008 (или без)
Then в течение 10 секунд после load нет ни одного console.error
And console.warn от ET-008 (наш try/catch) не выстреливает
в нормальной работе
```
## AC-12. Маркер показывается ДО DOMContentLoaded
```
Given пользователь грузит страницу с ?smoke=et-008 в медленной сети
When браузер ещё парсит <body>
Then инлайн-скрипт в <head> уже выставил html.smoke-mode
And нет FOUC (маркер не мигает «скрыт → видим» после загрузки)
```
## AC-13. Не ломает существующий функционал
```
Given изменения ET-008 применены
When пользователь использует
роутинг (Маршрут), связку, красивый, разведку, линейку,
поиск, метку, GPX, единицы измерения (км/мили),
переключение темы, тени рельефа, переключение POI
Then все режимы работают как до изменений
And нет регрессий в существующих E2E (ET-001..ET-007)
And нет ошибок в console.error при базовом сценарии
```
## AC-14. Откатываемость
```
Given изменения ET-008 применены и работают
When все упомянутые в TRZ §8 правки откатываются
Then приложение возвращается к состоянию до ET-008
And ни один тест из существующего набора не падает
And в DOM нет осиротевших элементов
And в CSS нет неиспользуемых правил для #pipeline-smoke
```
## AC-15. Изоляция диффа
```
Given коммит реализации ET-008
When анализируется его diff
Then изменены только два файла:
- src/web/index.html
- src/web/app.css
And app.js / units.js / gpx.js / style.json / style-dark.json
не затронуты
And ни одна строка backend, миграций, Docker-конфигов не изменена
```

View File

@@ -0,0 +1,300 @@
---
type: test-plan
work_item_id: ET-008
title: "Test Plan: Smoke test analyst integration"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
depends_on:
- "ET-008/02-trz.md"
- "ET-008/03-acceptance-criteria.md"
---
# План функциональных тестов — ET-008
scope:
- src/web/index.html
- src/web/app.css
out_of_scope:
- src/web/app.js
- src/web/units.js
- src/web/gpx.js
- src/web/style.json
- src/web/style-dark.json
- src/api/**
- migrations/**
- tests/api/**
test_suites:
- id: unit
name: "Unit-тесты статической структуры файлов"
runner: "node:test (без браузера)"
location: "tests/web/unit/pipeline-smoke.test.js"
cases:
- id: UT-01
title: "index.html содержит инлайн-скрипт активации smoke-mode"
ac_refs: [AC-03, AC-04, AC-12]
given:
- "файл src/web/index.html прочитан как строка"
when:
- "ищется подстрока 'smoke=et-008'"
- "ищется подстрока \"classList.add('smoke-mode')\""
then:
- "обе подстроки найдены"
- "скрипт расположен внутри <head> (по offset раньше </head>)"
- id: UT-02
title: "index.html содержит элемент #pipeline-smoke"
ac_refs: [AC-01]
given:
- "файл src/web/index.html прочитан как строка"
when:
- "ищется регулярка /id=\"pipeline-smoke\"/"
- "ищется текст 'ET-008 ✓'"
- "ищется atrribute aria-hidden=\"true\""
- "ищется attribute role=\"presentation\""
then:
- "все четыре поиска успешны"
- "элемент расположен внутри <body>, до <!-- Scripts -->"
- id: UT-03
title: "app.css содержит правила для #pipeline-smoke"
ac_refs: [AC-02, AC-03]
given:
- "файл src/web/app.css прочитан как строка"
when:
- "ищется селектор '#pipeline-smoke'"
- "ищется селектор 'html.smoke-mode #pipeline-smoke'"
- "ищется свойство 'display: none' для первого селектора"
- "ищется свойство 'position: fixed' для второго селектора"
then:
- "все четыре поиска успешны"
- id: UT-04
title: "Запрещённые файлы не изменены (whitelist diff)"
ac_refs: [AC-15]
given:
- "git diff feature/ET-008-... main"
when:
- "анализируется список изменённых файлов"
then:
- "ни один из {app.js, units.js, gpx.js, style.json, style-dark.json} не в diff"
- "ни один из {src/api/**, migrations/**, Dockerfile, docker-compose*.yml} не в diff"
- "в diff присутствуют только {src/web/index.html, src/web/app.css, docs/work-items/ET-008/**, tests/web/**/pipeline-smoke*}"
- id: integration
name: "Интеграционные тесты в jsdom"
runner: "node:test + jsdom"
location: "tests/web/integration/pipeline-smoke.test.js"
cases:
- id: IT-01
title: "Без smoke-параметра — класс не ставится"
ac_refs: [AC-02, AC-05, AC-06]
given:
- "jsdom инициализирован с URL https://localhost/enduro/"
- "загружен src/web/index.html (инлайн-скрипт исполнится синхронно)"
when:
- "проверяется document.documentElement.classList"
then:
- "класса 'smoke-mode' нет"
- id: IT-02
title: "С ?smoke=et-008 — класс ставится"
ac_refs: [AC-03]
given:
- "jsdom инициализирован с URL https://localhost/enduro/?smoke=et-008"
when:
- "загружен src/web/index.html"
then:
- "document.documentElement.classList.contains('smoke-mode') == true"
- id: IT-03
title: "С #smoke=et-008 в hash — класс ставится"
ac_refs: [AC-04]
given:
- "jsdom инициализирован с URL https://localhost/enduro/#smoke=et-008"
when:
- "загружен src/web/index.html"
then:
- "document.documentElement.classList.contains('smoke-mode') == true"
- id: IT-04
title: "С ?smoke=et-007 — класс не ставится"
ac_refs: [AC-05]
given:
- "jsdom URL .../?smoke=et-007"
when:
- "загружен index.html"
then:
- "классa 'smoke-mode' нет"
- id: IT-05
title: "С ?smoketest=et-008 — класс не ставится"
ac_refs: [AC-06]
given:
- "jsdom URL .../?smoketest=et-008"
when:
- "загружен index.html"
then:
- "класса 'smoke-mode' нет"
- id: IT-06
title: "Регистр имеет значение: ?smoke=ET-008 не активирует"
ac_refs: [AC-05]
given:
- "jsdom URL .../?smoke=ET-008"
when:
- "загружен index.html"
then:
- "класса 'smoke-mode' нет"
- id: e2e
name: "E2E (Playwright) — функциональные сценарии"
runner: "playwright"
location: "tests/web/e2e/pipeline-smoke.spec.ts"
base_url: "https://openclaw.mva154.duckdns.org/enduro/"
notes:
- "UI-визуальные тесты вынесены в 04b-ui-test-cases.md."
- "Эти тесты проверяют DOM-состояние и сетевые/console-эффекты, без скриншот-сравнений."
cases:
- id: E2E-01
title: "Маркер скрыт при чистой загрузке"
ac_refs: [AC-01, AC-02]
steps:
- "navigate base_url"
- "wait map idle (4000 ms)"
expect:
- "selector #pipeline-smoke exists"
- "evaluate: document.querySelector('#pipeline-smoke').textContent.trim() == 'ET-008 ✓'"
- "evaluate: getComputedStyle(document.querySelector('#pipeline-smoke')).display == 'none'"
- "evaluate: !document.documentElement.classList.contains('smoke-mode')"
- id: E2E-02
title: "Маркер видим при ?smoke=et-008"
ac_refs: [AC-03]
steps:
- "navigate base_url + '?smoke=et-008'"
- "wait map idle (4000 ms)"
expect:
- "evaluate: document.documentElement.classList.contains('smoke-mode')"
- "evaluate: getComputedStyle(document.querySelector('#pipeline-smoke')).display == 'inline-block'"
- "boundingClientRect(#pipeline-smoke).height > 0"
- id: E2E-03
title: "Маркер видим при #smoke=et-008"
ac_refs: [AC-04]
steps:
- "navigate base_url + '#smoke=et-008'"
- "wait map idle (4000 ms)"
expect:
- "evaluate: document.documentElement.classList.contains('smoke-mode')"
- "evaluate: getComputedStyle(document.querySelector('#pipeline-smoke')).display == 'inline-block'"
- id: E2E-04
title: "Маркер скрыт при неверном значении (?smoke=et-007)"
ac_refs: [AC-05]
steps:
- "navigate base_url + '?smoke=et-007'"
- "wait map idle (4000 ms)"
expect:
- "evaluate: !document.documentElement.classList.contains('smoke-mode')"
- "evaluate: getComputedStyle(document.querySelector('#pipeline-smoke')).display == 'none'"
- id: E2E-05
title: "Совместимость с переключением темы"
ac_refs: [AC-08]
steps:
- "navigate base_url + '?smoke=et-008'"
- "wait map idle (4000 ms)"
- "click #btn-theme"
- "wait 1500 ms"
- "click #btn-theme"
- "wait 1500 ms"
expect:
- "после каждого переключения темы getComputedStyle(#pipeline-smoke).display == 'inline-block'"
- "background-color и color #pipeline-smoke не зависят от темы"
- id: E2E-06
title: "Отсутствие новых сетевых запросов"
ac_refs: [AC-10]
steps:
- "register page.on('request') accumulator"
- "navigate base_url + '?smoke=et-008'"
- "wait map idle (4000 ms)"
expect:
- "среди собранных URL нет /api/smoke, /api/et-008, /smoke/*"
- "количество запросов сопоставимо с базовой загрузкой без ET-008 (± 5%)"
- id: E2E-07
title: "Отсутствие console.error / console.warn от ET-008"
ac_refs: [AC-11]
steps:
- "register page.on('console')"
- "navigate base_url + '?smoke=et-008'"
- "wait 10000 ms"
expect:
- "0 console.error сообщений за время теста"
- "0 console.warn сообщений, чей text содержит 'ET-008' или 'smoke'"
- id: E2E-08
title: "ARIA-атрибуты выставлены корректно"
ac_refs: [AC-07]
steps:
- "navigate base_url + '?smoke=et-008'"
- "wait 4000 ms"
expect:
- "getAttribute(#pipeline-smoke, 'aria-hidden') == 'true'"
- "getAttribute(#pipeline-smoke, 'role') == 'presentation'"
- id: E2E-09
title: "Клики проходят сквозь маркер"
ac_refs: [AC-09]
steps:
- "navigate base_url + '?smoke=et-008'"
- "wait 4000 ms"
- "позиционировать курсор на координаты левого нижнего угла (10, 90vh)"
- "click"
expect:
- "клик зарегистрирован картой (например, map.on('click') триггерится)"
- "маркер не получил focus, не вызвал событий"
- id: E2E-10
title: "Не ломает существующий набор E2E"
ac_refs: [AC-13]
steps:
- "запустить полный набор tests/web/e2e/**"
expect:
- "все ранее зелёные тесты остаются зелёными"
coverage_matrix:
AC-01: [UT-02, E2E-01]
AC-02: [UT-03, IT-01, E2E-01]
AC-03: [UT-01, UT-03, IT-02, E2E-02]
AC-04: [UT-01, IT-03, E2E-03]
AC-05: [IT-04, IT-06, E2E-04]
AC-06: [IT-05]
AC-07: [UT-02, E2E-08]
AC-08: [E2E-05]
AC-09: [E2E-09]
AC-10: [E2E-06]
AC-11: [E2E-07]
AC-12: [UT-01]
AC-13: [E2E-10]
AC-14: "manual (revert + rerun E2E-10)"
AC-15: [UT-04]
exit_criteria:
- "Все unit-тесты UT-01..UT-04 зелёные"
- "Все integration-тесты IT-01..IT-06 зелёные"
- "Все E2E E2E-01..E2E-10 зелёные на test-окружении"
- "Существующий набор UI/E2E не имеет регрессий"
- "Покрытие AC ≥ 14 из 15 автотестами (AC-14 — ручная проверка отката)"
- "git diff показывает изменения только в src/web/index.html, src/web/app.css, docs/work-items/ET-008/**, tests/web/**/pipeline-smoke*"

View File

@@ -0,0 +1,230 @@
---
type: ui-test-cases
work_item_id: ET-008
title: "UI Test Cases: Smoke test analyst integration"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
depends_on:
- "ET-008/02-trz.md"
- "ET-008/03-acceptance-criteria.md"
---
# UI Test Cases — ET-008: Smoke test analyst integration
Playwright-сценарии для визуального тестирования. Базовый URL для всех
кейсов: `https://openclaw.mva154.duckdns.org/enduro/` (с добавлением
параметра smoke там, где это указано в шагах).
Селекторы взяты из текущего `src/web/index.html` и проектируемой
разметки нового маркера (`#pipeline-smoke`).
---
### TC-UI-01 — Маркер скрыт при чистой загрузке (desktop)
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. check-visual: "В левом нижнем углу карты НЕТ маркера 'ET-008 ✓'. Привычный UI не изменился: компас, GPX, локация, рельеф, тема — сверху-справа; нижний #toolbar центрирован"
4. screenshot: "et008-tc01-default-no-marker-desktop"
---
### TC-UI-02 — Маркер скрыт при чистой загрузке (mobile)
тип: ui
viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. check-visual: "В левом нижнем углу НЕТ маркера. Тулбар снизу и кнопки справа выглядят как обычно"
4. screenshot: "et008-tc02-default-no-marker-mobile"
---
### TC-UI-03 — Маркер виден при ?smoke=et-008 (desktop)
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 4000
3. check-visual: "В левом нижнем углу появился маленький полупрозрачный лейбл с текстом 'ET-008 ✓'. Высота ~16-20px, шрифт мелкий, фон тёмно-серый полупрозрачный"
4. screenshot: "et008-tc03-smoke-marker-visible-desktop"
---
### TC-UI-04 — Маркер виден при #smoke=et-008 (desktop)
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/#smoke=et-008
2. wait: 4000
3. check-visual: "Маркер 'ET-008 ✓' в левом нижнем углу присутствует, как и в TC-UI-03"
4. screenshot: "et008-tc04-smoke-marker-via-hash"
---
### TC-UI-05 — Маркер виден на мобильном
тип: ui
viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 4000
3. check-visual: "Маркер виден в левом нижнем углу, не перекрывает #toolbar (тулбар центрирован, маркер слева). Размер маркера такой же, как на desktop"
4. screenshot: "et008-tc05-smoke-marker-mobile"
---
### TC-UI-06 — Маркер скрыт при неверном значении
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-007
2. wait: 4000
3. check-visual: "В левом нижнем углу НЕТ маркера 'ET-008 ✓' (значение параметра не наш)"
4. screenshot: "et008-tc06-wrong-value-hidden"
---
### TC-UI-07 — Маркер в светлой теме
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 4000
3. click: "#btn-theme"
4. wait: 2000
5. check-visual: "Тема стала светлой (карта и панели светлые); маркер 'ET-008 ✓' в левом нижнем углу остался с тёмным полупрозрачным фоном, текст светлый — читается"
6. screenshot: "et008-tc07-smoke-marker-light-theme"
---
### TC-UI-08 — Маркер в тёмной теме
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 4000
3. check-visual: "Дефолтная тёмная тема: маркер 'ET-008 ✓' читается на тёмной карте за счёт собственного фона и контрастного текста"
4. screenshot: "et008-tc08-smoke-marker-dark-theme"
---
### TC-UI-09 — Не конфликтует с атрибуцией MapLibre
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 4000
3. check-visual: "Маркер слева внизу, атрибуция MapLibre справа внизу — они не перекрываются и не сливаются. Между ними чистая карта"
4. screenshot: "et008-tc09-no-overlap-with-attrib"
---
### TC-UI-10 — Не конфликтует с открытым sheet
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 4000
3. click: "#tb-route"
4. wait: 1500
5. check-visual: "Открылся bottom-sheet 'Маршрут'. Маркер либо полностью скрыт за sheet, либо виден только если sheet занимает не весь низ — никаких 'торчащих углов' маркера поверх sheet"
6. screenshot: "et008-tc10-sheet-overlap-route"
---
### TC-UI-11 — Не ломает кнопки правой панели
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 4000
3. check-visual: "Правая панель кнопок #map-controls-r (компас, GPX, локация, рельеф, тема) в прежнем виде; маркер слева внизу её не касается"
4. screenshot: "et008-tc11-right-controls-intact"
---
### TC-UI-12 — Не ломает GPX-панель
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 4000
3. click: "#tb-gpx"
4. wait: 1500
5. check-visual: "Bottom-sheet #sheet-gpx открыт, подсказка 'Нажми кнопку загрузки GPX...' читается. Маркер либо скрыт за sheet, либо виден слева и не пересекается с подсказкой"
6. screenshot: "et008-tc12-gpx-sheet-with-smoke"
---
### TC-UI-13 — Не ломает поиск
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 4000
3. click: "#tb-search"
4. wait: 1500
5. check-visual: "Поисковая панель/sheet открыт; маркер ET-008 слева внизу не мешает вводу и подсказкам"
6. screenshot: "et008-tc13-search-with-smoke"
---
### TC-UI-14 — Маркер виден при первом кадре (no FOUC)
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?smoke=et-008
2. wait: 500
3. screenshot: "et008-tc14-early-frame"
4. wait: 4000
5. screenshot: "et008-tc14-late-frame"
6. check-visual: "На обоих скриншотах (~500ms и ~4500ms) маркер ET-008 ✓ присутствует и стабильно расположен; никакого мигания между ранним и поздним кадром"
---
### TC-UI-15 — Регрессия: переключение темы без smoke (маркер всё ещё скрыт)
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. click: "#btn-theme"
4. wait: 2000
5. check-visual: "Тема переключилась; маркера 'ET-008 ✓' нет ни в одной из тем (поскольку smoke-параметра в URL не было)"
6. screenshot: "et008-tc15-no-smoke-after-theme-toggle"

View File

@@ -0,0 +1,88 @@
---
type: business-request
work_item_id: ET-009
title: "Final verification test — шкала масштаба на карте (dry-run пайплайна)"
created_at: 2026-05-31
source: pipeline-dry-run
requester: claude-bot
synthetic: true
---
# Бизнес-запрос — ET-009 (Final verification test)
## Контекст
Это **финальная dry-run верификация** пайплайна
`analyst → architect → coder → tester` после слияния PR #9
(«feat: add UI/visual testing to pipeline»). Реальный заказчик
отсутствует; запрос синтезирован агентом-аналитиком, чтобы прогнать
через весь конвейер минимально достаточную задачу с UI-составляющей
и проверить, что новая UI-тестовая ветка работает на «зелёной» задаче.
ET-007 (синтетический dry-run) проверял пайплайн на средней по размеру
задаче (новая подложка, переключатель, persistence, атрибуция).
ET-009 намеренно делает следующий шаг назад — **минимум кода, максимум
тестируемости**: одна строчка `map.addControl(new
maplibregl.ScaleControl(...))` плюс реакция на событие `unitchange` из
существующего модуля `Units` (ET-005).
Требования к синтетическому скоупу:
- Изменение изолировано во фронтенде (`src/web/`); backend и БД не
затрагиваются.
- Не ломает существующий функционал: карта, темы, роутинг, GPX,
рельеф (hillshade/TRI), POI, разведка, линейка, единицы измерения,
поиск, GPX-uploader, satellite-toggle (если ET-007 будет смержена
к моменту начала ET-009 — он не зависит, но и не конфликтует).
- Видно глазом — есть что проверить Playwright-ом (горизонтальная
шкала в нижнем левом углу карты, переключающая km↔mi синхронно с
кнопкой единиц измерения).
- Тривиально откатывается: одна правка в `src/web/app.js`
(35 строк) и опционально пара строк CSS.
## Исходная формулировка
> На карте сейчас нет визуальной шкалы масштаба. Хочется простую
> горизонтальную линейку в углу карты, чтобы при планировании
> маршрута можно было «на глаз» оценить расстояние без линейки и без
> переключения в режим расчёта. Шкала должна автоматически
> подстраиваться под текущий выбор единиц (км/мили) — он уже есть в
> терреин-попапе (ET-005). Никаких настроек, никаких кнопок — просто
> всегда видимый элемент в углу.
## Уточнения (приняты по умолчанию для dry-run)
1. Контрол: встроенный `maplibregl.ScaleControl` — он же используется
в туториалах MapLibre, не требует внешних зависимостей.
2. Расположение: **bottom-left** угла карты (левый нижний). Это
единственный угол карты, который сейчас свободен:
- top-left занят `NavigationControl` + `FullscreenControl`
(`src/web/app.js:1403-1404`);
- top-right занят правой кнопочной панелью `#map-controls-r`;
- bottom-right занят `AttributionControl` (compact).
3. Единицы: при первой загрузке шкала использует
`Units.getUnit()``'metric'` (для `'km'`) или `'imperial'`
(для `'mi'`). При событии `unitchange` (диспатчится из
`units.js:108-114`) шкала перестраивается в новой единице.
4. Максимальная ширина: 100px (дефолт MapLibre).
5. Тема: цвет границы и текста шкалы должны быть читаемы на обеих
темах (день/ночь). При необходимости — лёгкий CSS-override в
`app.css` через переменные `var(--text)` / `var(--surface)`.
6. Совместимость с переключением темы (`map.setStyle()`): контролы
MapLibre переживают `setStyle()` автоматически — никаких ручных
действий внутри `rebuildMapOverlays()` не требуется. Однако
обработчик `unitchange` должен оставаться рабочим (он навешан на
`document`, не на `map`, поэтому переживёт).
7. Backend / БД / тайл-эндпоинты / OSRM не затрагиваются.
8. Доступность: шкала — декоративный элемент (`aria-hidden="true"` по
умолчанию у `maplibregl-ctrl-scale`). Дополнительно ничего делать
не нужно.
## Не входит в скоуп
- Кликабельность шкалы / переключение единиц по клику в неё.
- Вертикальная шкала, линейка с делениями.
- Сохранение настройки «показывать шкалу» — она всегда видна.
- Локализация подписей шкалы (используем стандартные MapLibre:
`km` / `m` / `mi` / `ft` — английские, как в библиотеке).
- Backend-эндпоинт настроек пользователя.

View File

@@ -1,7 +1,103 @@
# Business Request: E2E final test analyst sync
---
type: business-request
work_item_id: ET-010
title: "Final analyst-sync test — координаты центра карты в scale-zoom-bar (dry-run)"
created_at: 2026-05-31
source: pipeline-dry-run
requester: claude-bot
synthetic: true
---
Work Item ID: ET-010
# Бизнес-запрос — ET-010 (Final analyst-sync test)
## Description
## Контекст
TBD
Это **финальная dry-run верификация** пайплайна
`analyst → architect → coder → tester` после слияния PR #9
(«feat: add UI/visual testing to pipeline»). Реальный заказчик
отсутствует; запрос синтезирован агентом-аналитиком, чтобы прогнать
через весь конвейер минимально достаточную задачу с UI-составляющей
и проверить, что:
1. Полный набор артефактов аналитика (BRD/ТЗ/AC/test-plan/UI-test-cases)
создаётся, валидируется и синхронизируется в работу.
2. UI-тестовая ветка пайплайна (`04b-ui-test-cases.md` → Playwright)
стабильно работает на «зелёной» задаче.
3. Артефакты не конфликтуют параллельно с ET-009 (шкала масштаба в
`bottom-left`).
ET-007 проверял пайплайн на средней задаче (новая подложка,
переключатель, persistence, атрибуция). ET-009 — на минимальной
(MapLibre `ScaleControl` + реакция на `unitchange`). ET-010 — ещё один
точечный шаг: **расширение существующего custom-overlay
`#scale-zoom-bar`** на ещё одну строку с координатами центра карты,
обновляющимися на `map.on('move')`.
Требования к синтетическому скоупу:
- Изменение изолировано во фронтенде (`src/web/`); backend и БД не
затрагиваются.
- Не ломает существующий функционал: карта, темы, роутинг, GPX,
рельеф (hillshade/TRI), POI, разведка, линейка, единицы измерения,
поиск, GPX-uploader.
- Не конфликтует с ET-009: ET-010 трогает существующий
`#scale-zoom-bar` (top-right), ET-009 добавляет нативный
`ScaleControl` (bottom-left). Зон пересечения нет.
- Видно глазом — есть что проверить Playwright-ом (новый текстовый
блок «55.500, 40.500» в верхнем правом углу, обновляющийся при
перемещении/зуме карты).
- Тривиально откатывается: одна правка в `src/web/app.js` (510 строк)
плюс 35 строк CSS.
## Исходная формулировка
> При планировании эндуро-маршрута часто нужно «снять координаты»
> точки на карте — например, чтобы переслать её в чат или вбить в
> навигатор. Сейчас координаты можно увидеть только в адресной
> строке (URL hash `#zoom/lat/lon`), это неудобно. Хочется компактный
> индикатор широты/долготы центра карты — чтобы при наведении/центровке
> карты на нужную точку можно было сразу прочитать её координаты.
> Никаких настроек, никаких кнопок — просто всегда видимый ненавязчивый
> текст рядом со шкалой и индикатором зума.
## Уточнения (приняты по умолчанию для dry-run)
1. Что показываем: **широта и долгота центра карты** (`map.getCenter()`),
а не координаты курсора. Курсор не работает на тач-устройствах,
а пользователь и так центрирует карту на нужной точке.
2. Источник событий: `map.on('move', updateCenterCoords)`. Этот же
паттерн уже используется для `scale-zoom-bar` (`map.on('move')`
и `map.on('zoom')` обновляют шкалу и `z7`).
3. Местоположение: **внутри существующего `#scale-zoom-bar`** (top-right),
третьим под-элементом `.szb-coords` после `.szb-scale` и `.szb-zoom`.
Новый DOM-элемент создаётся той же функцией, что создаёт scale-zoom-bar
(`initMap`), порядок строго `scale → zoom → coords`.
4. Формат координат: `"55.500, 40.500"` — широта, запятая+пробел,
долгота. Три знака после точки (≈ 100 м точности на широте 55°).
Без подписей «N/E/S/W» — это технический индикатор для пилота.
Отрицательные координаты — со знаком минус.
5. Округление: `toFixed(3)`. При значениях |x| < 1 показываем
`0.000` (а не `.000`).
6. Тема: цвет текста — `var(--text)` (как у `.szb-label` и `.szb-zoom`).
Фон — тот же `var(--surface)` (наследуется от родителя `.szb-*`).
7. Доступность: декоративный текст. `aria-hidden="true"`. Голосовому
ассистенту читать координаты бессмысленно (их шесть знаков
меняются почти при каждом движении).
8. Производительность: на `map.on('move')` срабатывает уже
`updateScaleZoom()`. Координаты обновляются **в той же функции**
(одна точка обновления, один pass по DOM). Никакого debouncing не
требуется (текст коротенький, перерасчёт O(1)).
9. localStorage: ничего не сохраняем — координаты вычисляются «на
лету» из текущего центра карты.
10. Backend / БД / тайл-эндпоинты / OSRM не затрагиваются.
## Не входит в скоуп
- Координаты курсора при `mousemove` (тач не работает).
- Копирование координат по клику в clipboard.
- Переключение формата (десятичные ↔ DMS).
- Локализация (`N/E/S/W`) — оставляем чистые числа.
- Сохранение «показывать/скрывать координаты» — они всегда видны.
- Отображение высоты центра карты (это PH-8 Elevation Profile).
- Поведение в режиме линейки — координаты центра карты продолжают
обновляться, индикатор линейки `#ruler-info` не трогаем.

View File

@@ -0,0 +1,123 @@
---
type: brd
work_item_id: ET-010
title: "BRD: Координаты центра карты в scale-zoom-bar"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
---
# BRD — ET-010: Координаты центра карты в scale-zoom-bar
## 1. Цель
Дать пилоту-эндуристу возможность мгновенно «снять» координаты любой
точки маршрута, центрировав на ней карту — без открытия адресной
строки браузера, без копирования из URL-хеша и без переключения в
сторонние навигаторы.
Дополнительно — эта работа служит **end-to-end final dry-run**
проверкой пайплайна `analyst → architect → coder → tester` после
слияния PR #9 (UI/visual testing). Цели dry-run:
- Подтвердить, что полный пакет артефактов аналитика
(BRD + ТЗ + AC + test-plan + UI-test-cases) собирается,
валидируется и синхронизируется в pipeline без ручного
вмешательства.
- Подтвердить, что UI-составляющая (Playwright + screenshot)
стабильна на «зелёной» минимальной задаче.
- Подтвердить, что параллельные dry-run работы (ET-009 / ET-010) не
конфликтуют по зонам ответственности.
## 2. Контекст
- Веб-приложение: MapLibre GL JS 4.7 + vanilla JS, без фреймворка.
- Базовая страница: `src/web/index.html`; стили — `src/web/app.css`;
логика — `src/web/app.js`.
- В `src/web/app.js:1405-1409` уже создаётся custom-overlay
`#scale-zoom-bar` (top-right) с двумя суб-элементами:
- `.szb-scale` — горизонтальная шкала с подписью «30 km»;
- `.szb-zoom` — зум-индикатор «z7».
Обновление обоих делает `updateScaleZoom()`, навешанная на
`map.on('move')` и `map.on('zoom')`.
- Координаты центра карты уже доступны через `map.getCenter()`
`{ lng, lat }`.
- Параллельная задача ET-009 добавляет нативный
`maplibregl.ScaleControl` в `bottom-left`. Не пересекается с ET-010.
- Темы (день/ночь): переключение реализовано через `switchMapStyle()`
`map.setStyle()``rebuildMapOverlays()`. Кастомный
`#scale-zoom-bar` существует независимо от стиля MapLibre и не
затирается при `setStyle()`.
- Backend и БД не затрагиваются.
## 3. Scope
### In scope
| # | Функция |
|------|-------------------------------------------------------------------------------|
| F-01 | Новый суб-элемент `.szb-coords` внутри `#scale-zoom-bar` |
| F-02 | Текст элемента — `"<lat>, <lon>"` с тремя знаками после запятой |
| F-03 | Обновление координат происходит в существующей `updateScaleZoom()` |
| F-04 | Координаты обновляются на `map.on('move')` (включая зум) |
| F-05 | Цвет / шрифт / выравнивание — совместимы с соседними `.szb-*` на обеих темах |
| F-06 | Поведение при `setStyle()` (тема): `#scale-zoom-bar` не пересоздаётся, текст остаётся |
| F-07 | Доступность: `.szb-coords` имеет `aria-hidden="true"` (декоративный элемент) |
| F-08 | Координаты обновляются и в десктопном, и в мобильном viewport |
### Out of scope
- Координаты курсора при `mousemove` (тач не работает).
- Копирование координат по клику в clipboard.
- Форматы координат, отличные от десятичных (DMS, MGRS).
- Локализация (буквы N/E/S/W) — показываем чистые числа.
- Высота центра карты (это PH-8 Elevation Profile).
- Сохранение «показывать/скрывать координаты» в `localStorage`.
- Изменение поведения индикатора линейки `#ruler-info`.
- Backend-эндпоинт настроек пользователя.
## 4. Метрики успеха
| Метрика | Критерий |
|-----------------------------------------------|-------------------------------------------------------------------------------------------|
| Видимость индикатора | Элемент `.szb-coords` присутствует в DOM на каждой загрузке |
| Дефолтное значение | После загрузки на дефолтной позиции (`55.5, 40.5`) текст — `"55.500, 40.500"` |
| Реакция на panning | После `map.panTo([41.0, 56.0])` текст становится `"56.000, 41.000"` (± 0.001) |
| Реакция на зум | При зум-ин/зум-аут центр карты тот же → координаты не меняются (а вот `.szb-zoom` меняется) |
| Формат | Всегда три знака после точки, разделитель `", "`, без `N/E/S/W` |
| Отрицательные координаты | Лондон (`51.5, -0.1`) показывается как `"51.500, -0.100"` |
| Совместимость с темой | После переключения темы (`#btn-theme`) текст продолжает обновляться, цвет читаем |
| Совместимость с overlay-ями | После загрузки GPX / построения маршрута / переключения рельефа `.szb-coords` обновляется |
| Производительность | На пять последовательных `panBy` подряд UI не «лагает», нет ошибок в `console.error` |
| Не ломает существующий функционал | Шкала и зум-индикатор продолжают работать как раньше |
| Доступность | `.szb-coords` имеет `aria-hidden="true"`; скринридер не зачитывает координаты |
## 5. Риски
| Риск | Вероятность | Влияние | Митигация |
|---------------------------------------------------------------------|-------------|---------|------------------------------------------------------------------------------------|
| Конфликт с ET-009 (`ScaleControl` в `bottom-left`) | Низкая | Низкое | Разные зоны экрана; никаких общих DOM-узлов |
| `#scale-zoom-bar` пропадает при `setStyle()` | Низкая | Низкое | Элемент уже устойчив к `setStyle()` (существующий код); проверяется AC-05 |
| Текст координат «прыгает» по ширине (3 → 4 знака) | Средняя | Низкое | Фиксируем `toFixed(3)` всегда — ширина строки стабильна |
| На мобильном `#scale-zoom-bar` становится узким, координаты не помещаются | Средняя | Среднее | Используем `white-space: nowrap` и flex-обёртку; см. ТЗ §3 UI/UX |
| Слишком частые `map.on('move')` вызывают перерисовку DOM | Низкая | Низкое | Обновляем DOM только если значение текста реально изменилось |
| Скринридер начинает зачитывать обновляющиеся координаты | Низкая | Среднее | `aria-hidden="true"` на `.szb-coords` |
| Отрицательные координаты округляются неверно (`-0.0005 → "-0.000"`) | Низкая | Низкое | Используем `Number.parseFloat(x.toFixed(3))` или принимаем `"-0.000"` как корректно |
## 6. Зависимости
- **Внешние**: нет. Никаких новых сетевых запросов, API-ключей или
внешних сервисов.
- **Внутренние**: использует существующую функцию `updateScaleZoom()` и
существующий DOM-узел `#scale-zoom-bar` (`src/web/app.js:1405-...`).
- **Параллельные work item**:
- ET-007 (satellite map) — не затрагивает `#scale-zoom-bar`,
разные файлы (`style*.json`, `index.html`, новые функции). Конфликта нет.
- ET-009 (`ScaleControl` в `bottom-left`) — другой DOM-узел, другой
угол карты. Конфликта нет.
- **Файлы**: только `src/web/app.js` (расширение `updateScaleZoom()`
и создание `.szb-coords`) и `src/web/app.css` (стили `.szb-coords`).
- Backend, БД, OSRM, миграции, контейнеры — не затрагиваются.

View File

@@ -0,0 +1,219 @@
---
type: trz
work_item_id: ET-010
title: "ТЗ: Координаты центра карты в scale-zoom-bar"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
depends_on:
- "ET-010/01-brd.md"
---
# Техническое задание — ET-010: Координаты центра карты
## 1. Общая архитектура изменений
Изменение чисто фронтовое. Никакого нового кода в `src/api/`,
миграций или Docker-настроек не вносится. Затрагиваются **ровно два
файла**:
- `src/web/app.js` — расширение существующей функции
`updateScaleZoom()` (около строки 1411) и инициализация
суб-элемента `.szb-coords` рядом с `.szb-scale` / `.szb-zoom`.
- `src/web/app.css` — стили `.szb-coords` (по образцу `.szb-zoom`).
`src/web/index.html` **не трогаем**: `#scale-zoom-bar` создаётся
динамически в `initMap()`, в HTML его нет.
## 2. Функциональные требования
### REQ-F-01. Суб-элемент `.szb-coords`
В `initMap()` (`src/web/app.js`, рядом со строкой 1408), при сборке
`#scale-zoom-bar`, добавить третий блок:
```html
<div class="szb-scale"></div>
<div class="szb-zoom">z7</div>
<div class="szb-coords" aria-hidden="true">55.500, 40.500</div>
```
Порядок строго `scale → zoom → coords` (слева направо в flex-строке).
Стартовое значение — текущий центр карты после `new maplibregl.Map(...)`.
### REQ-F-02. Формат координат
```
"<lat>, <lon>"
```
Где:
- `lat` = `map.getCenter().lat.toFixed(3)` (три знака после точки)
- `lon` = `map.getCenter().lng.toFixed(3)` (три знака после точки)
- Разделитель — запятая + один пробел (`", "`)
- Знак минус сохраняется (`"-0.100"`)
- Без подписей N/E/S/W
Никаких других форматов не поддерживается.
### REQ-F-03. Обновление в `updateScaleZoom()`
Внутри существующей `updateScaleZoom()` после блока обновления
`zoomEl` (строка 1457) добавить:
```js
const coordsEl = scaleZoomBar.querySelector('.szb-coords');
if (coordsEl) {
const c = map.getCenter();
const next = `${c.lat.toFixed(3)}, ${c.lng.toFixed(3)}`;
if (coordsEl.textContent !== next) {
coordsEl.textContent = next;
}
}
```
Условие `if (coordsEl.textContent !== next)` обязательно — оно
исключает лишнюю работу с DOM при `move`-событиях, которые не сдвинули
центр карты заметно (например, конец инерции).
### REQ-F-04. Подключение к событиям
Никаких новых обработчиков навешивать **не нужно**. Существующие
`map.on('zoom', updateScaleZoom)` и `map.on('move', updateScaleZoom)`
(строки 1463-1464) автоматически вызывают `updateScaleZoom()` при
любом изменении вьюпорта, включая `panTo`, `flyTo`, `easeTo`, `zoomIn`,
`zoomOut`, перетаскивание мышью, pinch-zoom на тач-устройстве и
программный `map.jumpTo(...)`.
### REQ-F-05. Поведение при `setStyle()` (тема)
`#scale-zoom-bar` создаётся через `document.getElementById('map').appendChild(...)`
и не принадлежит DOM MapLibre — `setStyle()` его не затрагивает. После
переключения темы `updateScaleZoom()` вызывается следующим же
`map.on('move')` или `map.on('zoom')` — и текст координат корректно
обновляется. Дополнительных вызовов в `rebuildMapOverlays()` не нужно.
### REQ-F-06. Стили `.szb-coords`
В `src/web/app.css` после блока `.szb-zoom { … }` (после строки 926)
добавить:
```css
.szb-coords {
font-size: 10px;
font-weight: 500;
color: rgba(255,255,255,0.85);
text-shadow: 0 0 3px rgba(0,0,0,0.8), 0 1px 2px rgba(0,0,0,0.6);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
```
Семейство шрифта наследуется от `body`. `tabular-nums` гарантирует,
что строка не «прыгает» по ширине при изменении цифр.
### REQ-F-07. Доступность
- `.szb-coords` получает атрибут `aria-hidden="true"` сразу при
создании (см. REQ-F-01).
- Никакого `tabindex` элемент не получает.
- Не вмешивается в фокусную последовательность и не озвучивается
скринридером.
### REQ-F-08. Мобильный viewport
В существующем `#scale-zoom-bar { display: flex; gap: 6px; }` нет
переноса строк — все три суб-элемента остаются в одну линию. Если на
очень узких экранах (< 360 px) общая ширина превысит свободное место,
браузер обрежет правый край `.szb-coords` (с `nowrap`) — это
ожидаемое поведение dry-run, не требующее отдельной обработки.
В рамках ET-010 **не вводим** медиа-правил для скрытия `.szb-coords`
на мобильном. Если регрессия по верстке всё же обнаружится — fix
поедет отдельной задачей.
## 3. Нефункциональные требования
### REQ-NF-01. Производительность
- `updateScaleZoom()` вызывается синхронно из `map.on('move')` — это
уже факт текущего кода. Добавляемая работа: один `getCenter()`,
два `toFixed(3)`, одна конкатенация, одно сравнение, опционально
одно присваивание `textContent`. Все операции — O(1).
- Никаких новых `requestAnimationFrame` / `setTimeout` / `debounce`
не вводим.
- Бюджет: суммарная работа на `move` не должна расти больше чем на
+0.5 ms в среднем кадре (ориентир — `performance.now()` в DevTools).
### REQ-NF-02. Совместимость браузеров
- Поддержка: Chrome ≥ 110, Firefox ≥ 110, Safari ≥ 16, мобильные
Chrome/Safari iOS ≥ 16 (как у текущего приложения).
- `Number.prototype.toFixed`, `Array.prototype.querySelector`,
template-strings — поддержаны во всех целевых браузерах с 2017 года.
### REQ-NF-03. Логирование
- В нормальной работе никакого вывода в `console.*`.
- Никаких новых уровней логирования не вводим.
### REQ-NF-04. Совместимость с другими work item
- Не пересекается с ET-007 (satellite map): тот трогает
`style*.json`, `index.html`, `app.css`, добавляет источник
`sat-raster`. ET-010 не трогает ни источники, ни слои MapLibre.
- Не пересекается с ET-009 (ScaleControl bottom-left): тот добавляет
нативный MapLibre control в `bottom-left`. ET-010 расширяет
custom-overlay `#scale-zoom-bar` в `top-right`. Разные DOM-узлы.
## 4. Структура данных
Никаких новых ключей `localStorage`. Никаких новых полей в
`layerState`. Состояние существует только в DOM (`textContent`
элемента `.szb-coords`) и пересчитывается из `map.getCenter()` при
каждом вызове `updateScaleZoom()`.
## 5. UI / UX контракт
| Элемент | Местоположение | Поведение |
|------------------|-----------------------------------------------------------|--------------------------------------------------------------------------------------|
| `#scale-zoom-bar`| `top-right` карты (absolute, top ≈ 8 px, right 12 px) | Контейнер — не меняется визуально |
| `.szb-scale` | левый элемент flex-строки | Шкала «30 km» — не меняется |
| `.szb-zoom` | средний элемент flex-строки | «z7» — не меняется |
| `.szb-coords` | **правый элемент flex-строки** | «55.500, 40.500» — обновляется на `move`/`zoom` |
| Шрифт `.szb-coords` | `10px` / `500` / белый с тенью | Идентичен `.szb-label`, чуть мельче `.szb-zoom` |
| `aria-hidden` | `"true"` на `.szb-coords` | Скринридер игнорирует элемент |
## 6. Тестируемость
- В `tests/web/`: unit-тест функции форматирования (extract `formatCenterCoords(lat, lng)`
если оркестратор хочет — допустимо и инлайн-форматирование без extract).
- Integration-тест (jsdom): проверка, что после имитации
`updateScaleZoom()` `.szb-coords.textContent` соответствует
ожидаемой строке.
- E2E (Playwright) — см. `04-test-plan.yaml` и `04b-ui-test-cases.md`.
## 7. Совместимость и миграции
- БД: миграций нет.
- API: новых эндпоинтов нет.
- Конфиги: новых ENV не добавляем.
- Существующие work item: ET-005 (units), ET-006 (gpx), ET-007 (satellite),
ET-009 (scale control) не затрагиваются.
- Бизнес-конфиги, скрипты сборки, CI — не меняются.
## 8. Откат
В случае проблем:
1. В `src/web/app.js` удалить из `initMap()` строку создания
`.szb-coords` (часть `scaleZoomBar.innerHTML = '<div…><div class="szb-coords"…></div>'`).
2. Удалить из `updateScaleZoom()` блок обновления `coordsEl`
(5 строк).
3. В `src/web/app.css` удалить блок `.szb-coords { … }`.
Полный откат — один коммит, без побочных эффектов. Существующие шкала
и зум-индикатор продолжат работать как раньше.

View File

@@ -0,0 +1,186 @@
---
type: acceptance-criteria
work_item_id: ET-010
title: "AC: Координаты центра карты в scale-zoom-bar"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
depends_on:
- "ET-010/01-brd.md"
- "ET-010/02-trz.md"
---
# Критерии приёмки — ET-010: Координаты центра карты
Формат: Gherkin. Все сценарии — в браузере приложения (десктоп и
мобильный viewport, если не указано иное).
## AC-01. Элемент `.szb-coords` присутствует после загрузки
```
Given пользователь открывает приложение в браузере с чистым localStorage
When страница загружена и карта инициализирована (map.on('load'))
Then в DOM существует элемент #scale-zoom-bar .szb-coords
And элемент имеет атрибут aria-hidden="true"
And элемент находится в #scale-zoom-bar после .szb-zoom
And элемент содержит текст вида "<число>.<три цифры>, <число>.<три цифры>"
```
## AC-02. Дефолтные координаты совпадают с центром карты
```
Given карта инициализируется с center: [40.5, 55.5] (lng, lat)
When страница загружена и updateScaleZoom() отработал
Then .szb-coords.textContent == "55.500, 40.500"
```
Уточнение: при `hash: true` URL-фрагмент может переопределить центр —
тогда сравнение идёт с фактическим `map.getCenter()` после применения
hash.
## AC-03. Реакция на программный pan
```
Given карта в произвольном положении
When из консоли вызывается map.panTo([41.0, 56.0])
And карта закончила анимацию (map.once('moveend'))
Then .szb-coords.textContent == "56.000, 41.000" (± допуск 0.001 на координату)
```
## AC-04. Реакция на пользовательский drag
```
Given карта в произвольном положении X1
When пользователь перетаскивает карту мышью на 200 px влево
And отпускает кнопку
Then .szb-coords.textContent изменился (новое значение ≠ X1)
And формат строки соответствует "<lat>, <lon>" с тремя знаками после точки
```
## AC-05. Реакция на zoom без pan
```
Given карта в позиции (55.000, 40.000) с зумом z7
When пользователь зумит в (без панорамирования)
Then .szb-zoom.textContent изменился (z7 → z8 или больше)
And .szb-coords.textContent НЕ изменился (центр карты тот же)
```
## AC-06. Отрицательные координаты
```
Given карта установлена в Лондон (map.jumpTo({center: [-0.1, 51.5], zoom: 10}))
Then .szb-coords.textContent == "51.500, -0.100"
And знак минус виден перед нулём
```
## AC-07. Координаты вне Европы
```
Given карта установлена в Сидней (map.jumpTo({center: [151.2, -33.9], zoom: 10}))
Then .szb-coords.textContent == "-33.900, 151.200"
And положительная долгота показывается без знака
And отрицательная широта показывается со знаком
```
## AC-08. Совместимость с переключением темы
```
Given пользователь находится в светлой теме
And координаты в .szb-coords обновляются корректно
When пользователь переключает тему через #btn-theme
Then тема меняется (body.theme-dark)
And #scale-zoom-bar и .szb-coords остаются в DOM
And после первого же map.on('move') .szb-coords пересчитывается
And цвет текста читаем на новой подложке (контраст ≥ 4.5:1 на тёмном/светлом фоне за счёт text-shadow)
```
## AC-09. Совместимость с переключением единиц (ET-005)
```
Given пользователь переключает единицы измерения с «км» на «мили»
When срабатывает событие unitchange и updateScaleZoom() переотрисовывает .szb-scale
Then .szb-coords.textContent НЕ изменился
And формат координат не зависит от выбранной единицы (всегда десятичные градусы)
```
## AC-10. Совместимость с GPX-треком (ET-006)
```
Given пользователь загрузил GPX-файл и трек отрисовался на карте
When карта автоматически центрировалась на bbox трека (fitBounds)
Then .szb-coords.textContent отражает новый центр карты
And GPX-полилиния остаётся видимой
And статистика трека в #sheet-gpx остаётся доступной
```
## AC-11. Совместимость с активным маршрутом
```
Given пользователь построил маршрут (есть 2+ waypoint, отрисован вариант)
When пользователь перетаскивает карту
Then .szb-coords.textContent обновляется
And маршрутные линии остаются видимыми
And waypoint-маркеры на месте
```
## AC-12. Производительность под нагрузкой
```
Given карта в покое
When пользователь делает 10 быстрых map.panBy(...) подряд
Then нет ошибок в console.error
And UI остаётся отзывчивым (нет visible freeze > 100 ms)
And итоговое значение .szb-coords совпадает с map.getCenter() после последнего panBy
```
## AC-13. Доступность
```
Given пользователь использует скринридер (NVDA / VoiceOver — manual check)
When фокус перемещается по UI приложения через Tab
Then скринридер НЕ зачитывает содержимое .szb-coords
And .szb-coords не получает focus (нет tabindex)
And фокусная последовательность вокруг #map-controls-r не меняется
```
## AC-14. Не ломает существующий функционал
```
Given изменения ET-010 применены
When пользователь использует
роутинг (Маршрут), связку, красивый маршрут, разведку, линейку,
поиск, метку, GPX, единицы измерения (км/мили),
переключение темы, hillshade/TRI, POI
Then все режимы работают как до изменений
And нет регрессий в существующих UI-тестах (TC-UI-*)
And нет ошибок в console.error при базовом сценарии
And шкала .szb-scale и индикатор .szb-zoom продолжают обновляться
```
## AC-15. Откатываемость
```
Given изменения ET-010 применены и работают
When все упомянутые в TRZ §8 правки откатываются
Then приложение возвращается к состоянию до ET-010
And элемент .szb-coords отсутствует в DOM
And шкала и зум-индикатор работают корректно
And ни один тест из существующего набора не падает
And в DOM нет осиротевших элементов или ссылок на удалённые символы
```
## AC-16. Не конфликтует с ET-009
```
Given в ту же сборку случайно попали изменения ET-009 (ScaleControl bottom-left)
When страница загружена
Then в DOM присутствуют ОБА индикатора:
.szb-coords в #scale-zoom-bar (top-right) — от ET-010
.maplibregl-ctrl-scale в bottom-left — от ET-009
And они не перекрываются визуально
And оба обновляются синхронно с состоянием карты
```

View File

@@ -0,0 +1,333 @@
---
type: test-plan
work_item_id: ET-010
title: "Test Plan: Координаты центра карты в scale-zoom-bar"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
depends_on:
- "ET-010/02-trz.md"
- "ET-010/03-acceptance-criteria.md"
---
# План функциональных тестов — ET-010
scope:
- src/web/app.js
- src/web/app.css
out_of_scope:
- src/api/**
- migrations/**
- tests/api/**
- src/web/index.html
- src/web/style.json
- src/web/style-dark.json
test_suites:
- id: unit
name: "Unit-тесты форматирования координат"
runner: "vitest или node:test"
location: "tests/web/unit/center-coords.test.js"
cases:
- id: UT-01
title: "Формат: положительные координаты, целые градусы"
ac_refs: [AC-02, AC-03]
given:
- "lat = 55.5, lng = 40.5"
when:
- "вызван formatCenterCoords(lat, lng) или inline-эквивалент"
then:
- "результат == '55.500, 40.500'"
- id: UT-02
title: "Формат: отрицательные координаты"
ac_refs: [AC-06]
given:
- "lat = 51.5, lng = -0.1"
when:
- "вызван formatCenterCoords(lat, lng)"
then:
- "результат == '51.500, -0.100'"
- id: UT-03
title: "Формат: lat отрицателен, lng положителен (южное полушарие)"
ac_refs: [AC-07]
given:
- "lat = -33.9, lng = 151.2"
when:
- "вызван formatCenterCoords(lat, lng)"
then:
- "результат == '-33.900, 151.200'"
- id: UT-04
title: "Округление до трёх знаков"
ac_refs: [AC-02]
given:
- "lat = 55.50049, lng = 40.49951"
when:
- "вызван formatCenterCoords(lat, lng)"
then:
- "результат == '55.500, 40.500'"
- id: UT-05
title: "Околонулевые значения"
ac_refs: [AC-06]
given:
- "lat = 0, lng = 0"
when:
- "вызван formatCenterCoords(lat, lng)"
then:
- "результат == '0.000, 0.000'"
- id: UT-06
title: "Близкие к нулю отрицательные значения"
ac_refs: [AC-06]
given:
- "lat = -0.0001, lng = -0.0005"
when:
- "вызван formatCenterCoords(lat, lng)"
then:
- "результат содержит знак минус (например '-0.000, -0.001') либо без знака на чистом нуле — допускаем оба варианта; главное — формат '<lat>, <lng>' с тремя знаками"
- id: integration
name: "Интеграционные тесты updateScaleZoom (jsdom)"
runner: "vitest + jsdom"
location: "tests/web/integration/szb-coords.test.js"
cases:
- id: IT-01
title: "После initMap элемент .szb-coords создан с aria-hidden"
ac_refs: [AC-01]
given:
- "загружен src/web/app.js в jsdom"
- "мок window.maplibregl с MapStub (center: [40.5, 55.5])"
when:
- "вызван initMap()"
then:
- "document.querySelector('#scale-zoom-bar .szb-coords') существует"
- "элемент имеет aria-hidden='true'"
- "textContent соответствует регексу /^-?\\d+\\.\\d{3}, -?\\d+\\.\\d{3}$/"
- id: IT-02
title: "Порядок суб-элементов: scale → zoom → coords"
ac_refs: [AC-01]
given:
- "тот же setup"
when:
- "querySelectorAll('#scale-zoom-bar > *')"
then:
- "первый элемент имеет класс .szb-scale"
- "второй элемент имеет класс .szb-zoom"
- "третий элемент имеет класс .szb-coords"
- id: IT-03
title: "updateScaleZoom после программного pan обновляет textContent"
ac_refs: [AC-03]
given:
- "MapStub.center = {lng: 41.0, lat: 56.0}"
when:
- "вызван updateScaleZoom() напрямую"
then:
- ".szb-coords.textContent == '56.000, 41.000'"
- id: IT-04
title: "Идемпотентность: повторный вызов с тем же центром не перезаписывает DOM"
ac_refs: [REQ-NF-01]
given:
- "MapStub.center неизменён"
- "Сохранён MutationObserver на .szb-coords"
when:
- "updateScaleZoom() вызван 5 раз подряд"
then:
- "MutationObserver зафиксировал ≤ 1 запись об изменении textContent"
- id: IT-05
title: "Шкала и зум продолжают обновляться вместе с координатами"
ac_refs: [AC-14]
given:
- "MapStub.zoom = 10, center = {lng: 0, lat: 0}"
when:
- "обновляется MapStub: zoom = 12; вызван updateScaleZoom()"
then:
- ".szb-zoom.textContent == 'z12'"
- ".szb-scale текст обновился (label содержит цифры и единицу)"
- ".szb-coords.textContent == '0.000, 0.000'"
- id: e2e
name: "E2E (Playwright) — функциональные сценарии"
runner: "playwright"
location: "tests/web/e2e/center-coords.spec.ts"
base_url: "https://openclaw.mva154.duckdns.org/enduro/"
notes:
- "UI-визуальные тесты вынесены в 04b-ui-test-cases.md."
- "Эти тесты проверяют DOM-состояние и значения, без скриншот-сравнений."
cases:
- id: E2E-01
title: "Дефолтное состояние: .szb-coords присутствует"
ac_refs: [AC-01, AC-02]
steps:
- "navigate base_url"
- "evaluate: localStorage.clear()"
- "reload page"
- "wait map idle (3000 ms)"
expect:
- "selector #scale-zoom-bar .szb-coords exists"
- ".szb-coords attr aria-hidden == 'true'"
- "evaluate: .szb-coords.textContent matches /^-?\\d+\\.\\d{3}, -?\\d+\\.\\d{3}$/"
- id: E2E-02
title: "Программный panTo обновляет координаты"
ac_refs: [AC-03]
steps:
- "navigate base_url"
- "wait map idle"
- "evaluate: window._map.jumpTo({center: [41.0, 56.0], zoom: 8})"
- "wait 500 ms"
expect:
- "evaluate: document.querySelector('#scale-zoom-bar .szb-coords').textContent == '56.000, 41.000'"
- id: E2E-03
title: "Пользовательский drag обновляет координаты"
ac_refs: [AC-04]
steps:
- "navigate base_url"
- "wait map idle"
- "evaluate: const before = document.querySelector('.szb-coords').textContent; window.__before = before"
- "mouse.move в центр карты"
- "mouse.down"
- "mouse.move 200 px влево"
- "mouse.up"
- "wait 500 ms"
expect:
- "evaluate: document.querySelector('.szb-coords').textContent !== window.__before"
- "evaluate: /^-?\\d+\\.\\d{3}, -?\\d+\\.\\d{3}$/.test(document.querySelector('.szb-coords').textContent)"
- id: E2E-04
title: "Зум не сдвигает координаты центра"
ac_refs: [AC-05]
steps:
- "navigate base_url"
- "evaluate: window._map.jumpTo({center: [40, 55], zoom: 7})"
- "wait 500 ms"
- "evaluate: window.__coordsBefore = document.querySelector('.szb-coords').textContent; window.__zoomBefore = document.querySelector('.szb-zoom').textContent"
- "evaluate: window._map.zoomTo(10)"
- "wait 1000 ms"
expect:
- "evaluate: document.querySelector('.szb-coords').textContent == window.__coordsBefore"
- "evaluate: document.querySelector('.szb-zoom').textContent !== window.__zoomBefore"
- id: E2E-05
title: "Отрицательные координаты (Лондон)"
ac_refs: [AC-06]
steps:
- "navigate base_url"
- "evaluate: window._map.jumpTo({center: [-0.1, 51.5], zoom: 10})"
- "wait 500 ms"
expect:
- "evaluate: document.querySelector('.szb-coords').textContent == '51.500, -0.100'"
- id: E2E-06
title: "Южное полушарие (Сидней)"
ac_refs: [AC-07]
steps:
- "navigate base_url"
- "evaluate: window._map.jumpTo({center: [151.2, -33.9], zoom: 10})"
- "wait 500 ms"
expect:
- "evaluate: document.querySelector('.szb-coords').textContent == '-33.900, 151.200'"
- id: E2E-07
title: "Совместимость с переключением темы"
ac_refs: [AC-08]
steps:
- "navigate base_url"
- "evaluate: window._map.jumpTo({center: [40, 55], zoom: 7})"
- "wait 500 ms"
- "evaluate: window.__before = document.querySelector('.szb-coords').textContent"
- "click #btn-theme"
- "wait 2000 ms (style.load)"
- "evaluate: window._map.panBy([10, 0])"
- "wait 500 ms"
expect:
- "selector #scale-zoom-bar .szb-coords exists"
- "evaluate: document.querySelector('.szb-coords').textContent !== window.__before"
- id: E2E-08
title: "Совместимость с переключением единиц (ET-005)"
ac_refs: [AC-09]
steps:
- "navigate base_url"
- "wait map idle"
- "evaluate: window.__coordsBefore = document.querySelector('.szb-coords').textContent"
- "click #terrain-toggle"
- "wait 500 ms"
- "click #unit-btn-mi"
- "wait 500 ms"
expect:
- "evaluate: document.querySelector('.szb-coords').textContent == window.__coordsBefore"
- "evaluate: /mi/.test(document.querySelector('.szb-label').textContent)"
- id: E2E-09
title: "Производительность: 10 быстрых panBy без ошибок"
ac_refs: [AC-12]
steps:
- "navigate base_url"
- "wait map idle"
- "evaluate: for (let i=0;i<10;i++) window._map.panBy([50, 50], {duration: 0})"
- "wait 1000 ms"
expect:
- "no console.error captured"
- "evaluate: /^-?\\d+\\.\\d{3}, -?\\d+\\.\\d{3}$/.test(document.querySelector('.szb-coords').textContent)"
- id: E2E-10
title: "Не ломает существующий набор UI-тестов"
ac_refs: [AC-14]
steps:
- "запустить существующий набор E2E из tests/web/e2e/**"
expect:
- "все ранее зелёные тесты остаются зелёными"
- id: E2E-11
title: "Параллель с ET-009 не вызывает конфликта (опционально)"
ac_refs: [AC-16]
notes: "Запускать только если ET-009 уже смерженa в main"
steps:
- "navigate base_url"
- "wait map idle"
expect:
- "selector #scale-zoom-bar .szb-coords exists"
- "selector .maplibregl-ctrl-scale exists"
- "BoundingBox(.szb-coords) и BoundingBox(.maplibregl-ctrl-scale) не пересекаются"
coverage_matrix:
AC-01: [IT-01, IT-02, E2E-01]
AC-02: [UT-01, UT-04, UT-05, E2E-01]
AC-03: [IT-03, E2E-02]
AC-04: [E2E-03]
AC-05: [E2E-04]
AC-06: [UT-02, UT-06, E2E-05]
AC-07: [UT-03, E2E-06]
AC-08: [E2E-07]
AC-09: [E2E-08]
AC-10: [E2E-10]
AC-11: [E2E-10]
AC-12: [IT-04, E2E-09]
AC-13: "manual"
AC-14: [IT-05, E2E-10]
AC-15: "manual"
AC-16: [E2E-11]
exit_criteria:
- "Все unit-тесты UT-01..UT-06 зелёные"
- "Все integration-тесты IT-01..IT-05 зелёные"
- "Все E2E E2E-01..E2E-10 зелёные на test-окружении"
- "E2E-11 пропускается до мержа ET-009 — не блокирует"
- "Существующий набор UI/E2E не имеет регрессий"
- "Покрытие AC ≥ 14 из 16 автотестами (AC-13 и AC-15 — ручная проверка)"

View File

@@ -0,0 +1,305 @@
---
type: ui-test-cases
work_item_id: ET-010
title: "UI Test Cases: Координаты центра карты в scale-zoom-bar"
version: 1
status: draft
created_at: 2026-05-31
updated_at: 2026-05-31
authors:
- "agent:analyst"
depends_on:
- "ET-010/02-trz.md"
- "ET-010/03-acceptance-criteria.md"
---
# UI Test Cases — ET-010: Координаты центра карты
Playwright-сценарии для визуального тестирования. Базовый URL для всех
кейсов: `https://openclaw.mva154.duckdns.org/enduro/`.
Селекторы взяты из текущего `src/web/index.html` и предполагаемой
динамической разметки `#scale-zoom-bar` (создаётся в `initMap()`,
суб-элементы — `.szb-scale`, `.szb-zoom`, новый `.szb-coords`).
---
### TC-UI-01 — Элемент `.szb-coords` присутствует и читаем (desktop)
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. check-visual: "В верхнем правом углу карты, в одну строку со шкалой и индикатором зума, виден текст вида '55.500, 40.500'; шрифт идентичен соседним .szb-label и .szb-zoom; цвет белый с тенью"
4. screenshot: "et010-tc01-coords-desktop"
---
### TC-UI-02 — Элемент `.szb-coords` на мобильном
тип: ui
viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. check-visual: "В верхнем правом углу карты виден `#scale-zoom-bar` со шкалой, зумом и координатами; все три элемента в одну строку, не наезжают на правую кнопочную панель `#map-controls-r`"
4. screenshot: "et010-tc02-coords-mobile"
---
### TC-UI-03 — Дефолтные координаты совпадают с центром карты
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. check-visual: "Текст `.szb-coords` показывает координаты дефолтного центра карты России (55.5, 40.5) — что-то близкое к '55.500, 40.500' (если URL hash не переопределил)"
4. screenshot: "et010-tc03-default-coords"
---
### TC-UI-04 — Координаты обновляются при программном panTo
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. screenshot: "et010-tc04-before-pan"
4. evaluate: "window._map.jumpTo({center: [37.6173, 55.7558], zoom: 10}) // Moscow"
5. wait: 2000
6. check-visual: "Координаты в `.szb-coords` обновились на '55.756, 37.617' (± 0.001); шкала и зум-индикатор пересчитались соответственно"
7. screenshot: "et010-tc04-after-pan-moscow"
---
### TC-UI-05 — Координаты обновляются при пользовательском drag
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. screenshot: "et010-tc05-before-drag"
4. mouse-drag: from "center-of-#map" to "center-minus-200px-x"
5. wait: 1000
6. check-visual: "Координаты в `.szb-coords` изменились по сравнению с шагом 3; формат '<lat>, <lon>' с тремя знаками после точки сохранён; никаких лишних символов"
7. screenshot: "et010-tc05-after-drag"
---
### TC-UI-06 — Координаты не меняются при чистом зуме
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. evaluate: "window._map.jumpTo({center: [40, 55], zoom: 7})"
3. wait: 2000
4. screenshot: "et010-tc06-before-zoom"
5. evaluate: "window._map.zoomTo(10, {duration: 0})"
6. wait: 1500
7. check-visual: "В `.szb-zoom` значение увеличилось (z7 → z10); в `.szb-coords` остался прежний текст ('55.000, 40.000'); шкала ('.szb-scale') пересчиталась под новый зум"
8. screenshot: "et010-tc06-after-zoom"
---
### TC-UI-07 — Отрицательные координаты (Лондон)
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. evaluate: "window._map.jumpTo({center: [-0.1276, 51.5074], zoom: 10})"
3. wait: 2000
4. check-visual: "Текст `.szb-coords` показывает '51.507, -0.128' — со знаком минус перед долготой, три знака после точки"
5. screenshot: "et010-tc07-london"
---
### TC-UI-08 — Южное полушарие (Сидней)
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. evaluate: "window._map.jumpTo({center: [151.2093, -33.8688], zoom: 10})"
3. wait: 2000
4. check-visual: "Текст `.szb-coords` показывает '-33.869, 151.209' — отрицательная широта, положительная долгота без знака"
5. screenshot: "et010-tc08-sydney"
---
### TC-UI-09 — Совместимость с тёмной темой
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. check-visual: "Стартовая тема — тёмная (`body.theme-dark`); `.szb-coords` читаем поверх тёмной OSM-подложки благодаря text-shadow"
4. screenshot: "et010-tc09-coords-dark-theme"
---
### TC-UI-10 — Совместимость со светлой темой
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. click: "#btn-theme"
4. wait: 3000
5. check-visual: "Тема стала светлой; `.szb-coords` по-прежнему читаем; цвет текста не изменился (остался белым с тенью), за счёт `text-shadow` контраст сохраняется на светлом фоне"
6. screenshot: "et010-tc10-coords-light-theme"
---
### TC-UI-11 — Совместимость с переключением единиц измерения
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. click: "#terrain-toggle"
4. wait: 500
5. click: "#unit-btn-mi"
6. wait: 1500
7. check-visual: "В `.szb-label` единица изменилась на 'mi'; в `.szb-coords` формат и значения координат остались прежними (десятичные градусы)"
8. screenshot: "et010-tc11-coords-with-mi-units"
---
### TC-UI-12 — Совместимость с активным маршрутом
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. click: "#tb-route"
4. wait: 1500
5. click: "#map"
6. wait: 2500
7. scroll: 200
8. click: "#map"
9. wait: 4000
10. check-visual: "На карте отрисована маршрутная линия и две точки waypoint; `.szb-coords` в верхнем правом углу обновлён к новому центру карты; шкала и зум-индикатор корректны; mini-route-sheet с метриками виден"
11. screenshot: "et010-tc12-coords-with-route"
---
### TC-UI-13 — Совместимость с GPX-треком
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. click: "#tb-gpx"
4. wait: 1500
5. check-visual: "Bottom-sheet `#sheet-gpx` открыт; `.szb-coords` остаётся видимым над листом и продолжает отражать текущий центр карты"
6. screenshot: "et010-tc13-coords-with-gpx-sheet"
---
### TC-UI-14 — Совместимость с hillshade
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. evaluate: "window._map.jumpTo({center: [40.5, 55.5], zoom: 11})"
4. wait: 2000
5. click: "#terrain-toggle"
6. wait: 500
7. click: "#terrain-hillshade-cb"
8. wait: 3000
9. check-visual: "Поверх карты видны тени рельефа; `.szb-coords` по-прежнему читаемый, не перекрыт растром hillshade (находится в overlay-DOM, не на canvas)"
10. screenshot: "et010-tc14-coords-with-hillshade"
---
### TC-UI-15 — Стабильная ширина строки `.szb-coords`
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. evaluate: "window._map.jumpTo({center: [0.000, 0.000], zoom: 5})"
4. wait: 1000
5. screenshot: "et010-tc15a-zero-coords"
6. evaluate: "window._map.jumpTo({center: [-179.999, -89.999], zoom: 5})"
7. wait: 1000
8. check-visual: "Ширина блока `.szb-coords` визуально стабильна между шагами 5 и 7 (за счёт `font-variant-numeric: tabular-nums`); шкала и зум-индикатор не «прыгают» влево/вправо"
9. screenshot: "et010-tc15b-edge-coords"
---
### TC-UI-16 — Координаты на мобильном после drag
тип: ui
viewport: mobile
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. screenshot: "et010-tc16-before-touch-drag"
4. touch-drag: from "center-of-#map" to "center-plus-150px-y"
5. wait: 1500
6. check-visual: "`.szb-coords` обновился после тач-перетаскивания; шкала и зум-индикатор пересчитались; `#toolbar` снизу не перекрыл `#scale-zoom-bar`"
7. screenshot: "et010-tc16-after-touch-drag"
---
### TC-UI-17 — Регрессия: `.szb-coords` не перекрывает кнопки `#map-controls-r`
тип: ui
viewport: desktop
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. check-visual: "`#scale-zoom-bar` находится строго над кнопками `#map-controls-r`; нет visible overlap; кнопки компас/GPX-upload/locate/рельеф/тема кликабельны"
4. click: "#terrain-toggle"
5. wait: 500
6. check-visual: "Открылся `#terrain-popup`; `.szb-coords` остаётся видимым; popup не перекрывает координаты сверху"
7. screenshot: "et010-tc17-no-overlap"
---
### TC-UI-18 — Параллель с ET-009 (опциональный сценарий)
тип: ui
viewport: desktop
условия: "выполнять только если ET-009 уже смержен в main; иначе пропустить"
шаги:
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
2. wait: 4000
3. check-visual: "В верхнем правом углу — `.szb-coords` (ET-010); в нижнем левом углу — нативный `.maplibregl-ctrl-scale` (ET-009); элементы не перекрываются и не конфликтуют; оба обновляются при `panTo`"
4. evaluate: "window._map.jumpTo({center: [37.6173, 55.7558], zoom: 13})"
5. wait: 2000
6. check-visual: "После panTo оба индикатора обновились синхронно; на скриншоте видны и `.szb-coords '55.756, 37.617'`, и нативная шкала MapLibre с подписью в км/милях"
7. screenshot: "et010-tc18-coexistence-with-et009"