--- type: test-report work_item_id: ET-005 version: 1 status: pass tester: "agent:tester" date: 2026-05-21 commit_tested: 2fe5cfe verdict: PASS --- # Test Report — ET-005 ## Verdict: **PASS** → `stage:ready-to-deploy` Полный регресс зелёный: **pytest 31 passed, 4 skipped, 0 failed**; JS-юнит-тесты `units.js` **20/20 pass**; **e2e Playwright TP-01…TP-05 6/6 pass** (0 JS-ошибок на странице); lint чистый; тест-окружение отвечает 200. Блокирующих багов (P0/P1) не найдено. Весь тест-план `04-test-plan.yaml` (TP-01…TP-05) исполнен **в реальном браузере** — в отличие от ET-002, e2e не блокирован. Все 4 acceptance- критерия покрыты и не нарушены. ## Окружение - **Дата прогона:** 2026-05-21 - **Ветка:** `feature/ET-005-` - **Код-коммит:** `2fe5cfe` (`feat(web): переключатель единиц измерения расстояний (км/мили)`; источник — `12-review.md`) - **HEAD:** `d32ad8f` (`reviewer(ET): auto-commit ...`) — поверх кода только артефакт ревью, изменений кода нет; тестировалось рабочее дерево - **Python:** 3.12.13 - **pytest:** 8.3.3 (plugins: asyncio-1.3.0, anyio-4.13.0) - **Node:** v22.22.2 (`node --test`) - **Playwright:** 1.60.0, Chromium headless (chromium-headless-shell v1223) - **ruff:** установлен по `pyproject.toml [dev]` - **test-env:** https://openclaw.mva154.duckdns.org/enduro/ → HTTP 200 ## Healthcheck | Среда | URL | Код | |---|---|---| | local dev | http://localhost:5556/health | connection refused (dev не поднят — ОК, прогон оффлайн) | | test | https://openclaw.mva154.duckdns.org/enduro/ | 200 | ET-005 — фронтенд-изменение. В test задеплоен предыдущий код, поэтому healthcheck подтверждает только живость окружения; фича попадёт в test штатной перевыкладкой `src/web/` после merge (см. `07-infra-requirements.md §7`). e2e-прогон выполнен против **кода ветки**, поднятого локально статическим сервером (`python -m http.server` → `src/web/`), не против test-окружения. На prom ничего не запускалось. ## Команды запуска ```bash # Unit + integration (эквивалент make test) python -m pytest tests/ -v # JS behavioral unit-тесты units.js (TP-01..TP-04, AC-2, AC-3) node --test tests/unit/units.test.js # e2e (TP-01..TP-05): локальная раздача src/web + Playwright/Chromium python -m http.server 8777 --directory src/web & python /tmp/et005_e2e.py # Lint ruff check src/ tests/ ``` ## Результаты pytest `python -m pytest tests/ -v` → **31 passed, 4 skipped, 1 warning in 0.62s** | Файл | Тестов | PASS | SKIP | |---|---|---|---| | `integration/test_routing_barriers.py` | 7 | 3 | 4 | | `unit/test_health.py` | 1 | 1 | 0 | | `unit/test_poi_toggle.py` (ET-002, регресс) | 10 | 10 | 0 | | `unit/test_unit_toggle.py` (**ET-005**) | 17 | 17 | 0 | | **Итого** | **35** | **31** | **4** | **ET-005 — `test_unit_toggle.py` (17/17 PASS):** | Тест | Покрывает | Результат | |---|---|---| | `test_units_module_exists` | наличие `src/web/units.js` (ADR-0001 п.2) | **PASS** | | `test_units_module_public_api` | контракт `window.Units` (ADR-0001 п.3) | **PASS** | | `test_units_module_constants` | `KM_TO_MI=0.621371`, `distance_unit`, дефолт `km` | **PASS** | | `test_units_module_exports_for_browser_and_node` | `window.Units` + `module.exports` | **PASS** | | `test_unit_toggle_present_in_html` | кнопка km/mi в попапе (ФТ-1, AC-1) | **PASS** | | `test_unit_toggle_reuses_seg_control_component` | переиспользование `.seg-control` (R8) | **PASS** | | `test_units_js_loaded_before_app_js` | порядок скриптов (R7, ADR-0001 п.2) | **PASS** | | `test_unit_toggle_has_styles` | стили `.terrain-unit-row` (AC-4) | **PASS** | | `test_app_js_unit_functions_defined` | `onUnitToggle`/`syncUnitToggleUI`/`onUnitChange` | **PASS** | | `test_app_js_has_et005_block_markers` | блок-маркеры `>>> ET-005 ... >>>` | **PASS** | | `test_app_js_single_unitchange_subscription` | ровно одна подписка `unitchange` (ADR п.6) | **PASS** | | `test_app_js_uses_centralized_formatter` | форматирование через `Units.formatDistance` | **PASS** | | `test_app_js_distance_helpers_delegate_to_units` | хелперы делегируют в `units.js` (R1) | **PASS** | | `test_app_js_scale_bar_is_unit_aware` | scale-bar учитывает единицу (R3) | **PASS** | | `test_app_js_gpx_export_stays_metric` | GPX-экспорт остаётся метрическим (R6) | **PASS** | | `test_app_js_restores_unit_choice_on_load` | восстановление выбора при загрузке (AC-3) | **PASS** | | `test_js_unit_tests_pass` | запуск `units.test.js` через Node-раннер | **PASS** | **4 SKIP** — интеграционные тесты роутинга ET-001 (`test_routing_barriers.py::test_route_*`); требуют поднятого OSRM, недоступного в окружении тестера (штатный `skip`, чтобы CI без инфраструктуры не падал). ET-005 — фронтенд-изменение, на роутинг не влияет; к регрессу не относится. Предупреждение `PytestDeprecationWarning` (`asyncio_default_fixture_loop_scope`) — внешняя зависимость `pytest-asyncio`, к ET-005 отношения не имеет, не блокирует. ## Результаты JS unit-тестов `units.js` `node --test tests/unit/units.test.js` → **# tests 20, # pass 20, # fail 0** Тесты исполняют **реальный** `src/web/units.js` (сброс `require.cache` + инъекция моков `window`/`document`/`localStorage` перед каждым тестом). Покрыты TP-01…TP-04, AC-2, AC-3, граница 1000 м, недоступный `localStorage`, валидация `setUnit()`, публикация неймспейса, единый разделитель «запятая» (R4), точность по умолчанию. ## Результаты e2e (Playwright / Chromium) — TP-01…TP-05 Прогон в headless-Chromium против кода ветки, поднятого локально (`src/web/` через `http.server`). Взаимодействие — через **реальные DOM-клики** по кнопкам попапа (`onUnitToggle` срабатывает по inline `onclick`). Пересчёт видимых расстояний верифицирован на живой масштабной линейке карты (`#scale-zoom-bar`), которую перерисовывает оркестратор `onUnitChange()`. | TC | Сценарий | Факт | Результат | |---|---|---|---| | **TP-01** | дефолт после очистки `localStorage` | `getUnit()='km'`, кнопка «км» `.active`, «мили» нет, `localStorage`=пусто, `formatDistance(12345)='12,3 км'` | **PASS** | | **TP-02** | переключение в мили | `getUnit()='mi'`, «мили» `.active`, `localStorage='mi'`, `formatDistance(12345)='7,7 ми'`, scale-bar `'55 km'→'35 mi'` | **PASS** | | **TP-03** | persistence после reload | после перезагрузки `getUnit()='mi'`, «мили» `.active` | **PASS** | | **TP-04** | возврат в км | `getUnit()='km'`, «км» `.active`, `localStorage='km'`, scale-bar снова `'55 km'` | **PASS** | | **TP-05** | mobile responsive 375px | обе кнопки видимы и в пределах вьюпорта (km `x=166 w=57`, mi `x=226 w=57`), клик переключает | **PASS** | | NFR-perf | переключение < 100 мс | клик + пересчёт всех поверхностей = **0,5 мс** | **PASS** | **Итог e2e: 6/6 PASS.** На странице **не зафиксировано ни одной JS-ошибки** (`pageerror` за весь прогон — none). ## Покрытие тест-плана (04-test-plan.yaml) | TC | Тип | Исполнение | Статус | |---|---|---|---| | **TP-01** | e2e | Playwright + JS-тест `units.test.js` | **PASS** | | **TP-02** | e2e | Playwright (scale-bar `km→mi`) + JS-тест | **PASS** | | **TP-03** | e2e | Playwright (reload) + JS-тест | **PASS** | | **TP-04** | e2e | Playwright (scale-bar `mi→km`) + JS-тест | **PASS** | | **TP-05** | e2e | Playwright, viewport 375×667 | **PASS** | **Исполнено и пройдено: 5/5 тест-кейсов.** ## Соответствие Acceptance Criteria | AC | Описание | Источник проверки | Статус | |---|---|---|---| | **AC-1** | Кнопка km/mi в панели, показывает выбор, клик переключает | e2e TP-01 (км active по умолчанию), TP-02/TP-04 (клик переключает класс `.active`), `test_unit_toggle_present_in_html` | **PASS** | | **AC-2** | Пересчёт всех расстояний, коэф. 0.621371, округление до 1 знака | e2e TP-02 (scale-bar `55 km→35 mi`, `formatDistance(12345)=12,3 км→7,7 ми`), `units.test.js` (`KM_TO_MI`, точность 1 знак), `test_units_module_constants` | **PASS** | | **AC-3** | Сохранение/восстановление из `localStorage`, дефолт km | e2e TP-01 (дефолт km, `localStorage` пуст), TP-02 (`localStorage='mi'`), TP-03 (выживает reload) | **PASS** | | **AC-4** | Кнопка не перекрывает элементы, mobile, переключение < 100мс | e2e TP-05 (375px, кнопки в пределах вьюпорта, кликабельны), NFR-perf (0,5 мс ≪ 100 мс), `test_unit_toggle_has_styles` | **PASS** | Все 4 критерия имеют поведенческое покрытие в реальном браузере; ни один не нарушен. Коэффициент `0.621371` и округление до 1 знака подтверждены и unit-тестами на реальном `units.js`, и e2e-конвертацией. ## Найденные баги ### P0 (блокирующие) Нет. ### P1 (критические) Нет. ### P2 (важные) **T-01 (= R-01 из `12-review.md`) — переключение единиц сбрасывает выбор варианта связки.** В режиме связки `onUnitChange()` вызывает `renderLinkCards(linkRoutes)`, которая всегда подсвечивает «Вариант 1»; выбранный пользователем вариант 2/3 теряется при каждом переключении км/мили. Дефект **унаследован из ревью** (зафиксирован reviewer'ом как R-01/P2). **Тестером в этом прогоне не воспроизводился инструментально** — режим связки требует построенного маршрута и поднятого OSRM, недоступного в окружении (см. 4 SKIP). Расстояния при этом пересчитываются корректно, ФТ-3 ТЗ формально выполнено; дефект ограничен UX режима связки. **P2 — merge/деплой не блокирует.** Действие: dev — поправить в этом же PR (ввести `activeLinkIdx`) либо осознанно вынести в техдолг. ### P3 (косметика / наблюдения) 1. **(= R-02 из `12-review.md`)** Масштабная линейка в режиме «mi» использует латиницу и точку (`'0.5 mi'`) вместо запятой и русских подписей `units.js` (`'0,5 ми'`). Это **пред-существующее** поведение scale-bar (в режиме «km» и раньше было `'30 km'`), ET-005 лишь расширил тот же стиль на мили — регрессии нет. e2e подтвердил: scale-bar корректно меняет суффикс `km↔mi`. Косметика, не блокирует. 2. **(= R-03 из `12-review.md`)** Слой `app.js` (оркестратор, unit-aware ветка scale-bar) в репозитории покрыт только статикой. В этом прогоне пробел закрыт **e2e**: оркестратор проверен на живой масштабной линейке (TP-02/TP-04). Перерисовка карточек маршрута/связки через `onUnitChange()` инструментально не покрыта (нет OSRM); поведение подтверждено `units.test.js` на реальном коде + статикой `test_app_js_*`. Техдолг на DOM/MapLibre-харнесс для `app.js` остаётся. 3. **Окружение тестера.** Пакеты `shapely`, `mapbox-vector-tile` (`requirements.txt`) и `pytest-asyncio`, `ruff` (`pyproject.toml [dev]`) не были предустановлены в песочнице — без `shapely` падал сбор `test_health.py` (импорт `src.api.main`). Тестер доустановил их по манифестам проекта. Это дефект провижининга окружения, **не дефект ET-005**. CI обязан выполнять `pip install -r requirements.txt` и `.[dev]` перед `make test`. ## Замечания тестера - **e2e-инструментарий.** Playwright + Chromium установлены **только в песочницу тестера** для исполнения e2e. В артефакты проекта (`requirements.txt`, `pyproject.toml`, `package.json`) ничего **не добавлено** — ограничение `07-infra-requirements.md §6` («новые npm/Python пакеты — Нет», касается production/build-зависимостей) **не нарушено**. e2e-тесты `04-test-plan.yaml` (TP-01…TP-05) явно ожидаются `07-infra-requirements.md §9`; здесь они исполнены без изменения кода и тестов проекта. Скрипт прогона — временный, в репозиторий не коммитится. - Ручная сверка реализации: `index.html:62-69` — сегментированный переключатель `#unit-seg` (кнопки `unit-btn-km`/`unit-btn-mi`) в попапе `#terrain-popup` после чекбокса POI, отделён `