diff --git a/docs/work-items/ET-005/13-test-report.md b/docs/work-items/ET-005/13-test-report.md new file mode 100644 index 0000000..aaba56a --- /dev/null +++ b/docs/work-items/ET-005/13-test-report.md @@ -0,0 +1,247 @@ +--- +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, отделён `