analyst(ET-008): BRD, TRZ, AC, TestPlan, UI tests v2
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
type: brd
|
||||
work_item_id: ET-008
|
||||
title: "BRD: GPS-треки с публичных платформ на карте"
|
||||
version: 1
|
||||
version: 2
|
||||
status: draft
|
||||
created_at: 2026-06-01
|
||||
updated_at: 2026-06-01
|
||||
changelog:
|
||||
- "v2 (2026-06-01): полная переработка под реальный business request — серверная агрегация из ≥3 источников по региону, дедупликация, фильтры по активности и источнику, расширяемость на регионы. Предыдущая v1 трактовала задачу как URL-импорт + OSM live-поиск, что не соответствовало бизнес-цели."
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
---
|
||||
@@ -14,85 +16,213 @@ authors:
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Дать пользователю возможность увидеть на карте Enduro Trails GPS-треки
|
||||
с публичных источников без скачивания файлов вручную: либо вставив
|
||||
прямую ссылку на GPX, либо найдя чужие публичные треки в видимой
|
||||
области карты (через OSM Public GPS Traces).
|
||||
Показать пользователю Enduro Trails реальные GPS-треки, **заранее
|
||||
собранные с публичных платформ** (Wikiloc, Offmaps.ru, Тропинки.ру,
|
||||
EnduroRussia.ru, OSM Public GPS Traces, Nakarte.me, Komoot и т.п.) и
|
||||
сохранённые на сервере. Цель — три практические задачи мотоэндуриста:
|
||||
|
||||
1. **Видеть реальные дороги/тропы, которых нет в OSM.** Vector-тайлы
|
||||
`trails` показывают только OSM-данные; реальные грунтовки/тропы из
|
||||
GPS-логов дают информацию, которой в OSM никогда не было.
|
||||
2. **Понимать, где реально ездят.** Плотность публичных треков на
|
||||
участке — прямая прокси-метрика популярности и проходимости.
|
||||
3. **Выявлять «мёртвые» дороги.** OSM-грунтовка, не покрытая ни одним
|
||||
публичным треком за последние N лет — кандидат на «давно никто не
|
||||
ездит, может быть заросла».
|
||||
|
||||
ET-008 даёт **новый отдельный слой** (поверх `trails`, ниже маршрута
|
||||
OSRM) с отдельными линиями (не heatmap), цветом по источнику или типу
|
||||
активности, с UI-фильтрами.
|
||||
|
||||
## 2. Контекст
|
||||
|
||||
- ET-006 реализовал клиентский GPX-стек: парсер, модель
|
||||
`window.gpxTracks`, sheet `#sheet-gpx`, статистика, профиль высот,
|
||||
переживание `map.setStyle()` через `rebuildGpxOverlays()`. Источник
|
||||
данных — только локальный файл пользователя.
|
||||
- Roadmap-фаза PH-3 «Smart Route» включает работу с GPX (импорт/экспорт).
|
||||
- В стеке нет пользовательских аккаунтов и БД пользователей. Платформы с
|
||||
обязательным OAuth (Strava, Komoot) поэтому вне scope текущей итерации.
|
||||
- Браузер не может тянуть GPX напрямую с большинства публичных платформ
|
||||
из-за CORS. OSM API не разрешает кросс-доменные запросы → прокси
|
||||
через FastAPI обязателен.
|
||||
- OSM Public GPS Traces — открытый бесплатный источник публичных
|
||||
GPS-треков, формат GPX, есть bbox-поиск, нет авторизации для чтения.
|
||||
- Vector-тайлы из OSM (`/api/tiles/{z}/{x}/{y}.mvt`) уже отдают
|
||||
грунтовки/тропы/POI. ET-008 их **не заменяет** — добавляет
|
||||
параллельный слой публичных GPS-треков.
|
||||
- ET-006 реализовал клиентский импорт GPX-файлов пользователем
|
||||
(`window.gpxTracks`, `#sheet-gpx`). Это **другой сценарий**: ET-006 —
|
||||
«мой трек в памяти браузера», ET-008 — «треки сообщества с сервера».
|
||||
Модели данных не пересекаются.
|
||||
- Стек БД: SQLite + Spatialite. Для ET-008 заводится **отдельная** БД
|
||||
`data/gps_tracks.sqlite` — чтобы не смешивать данные с основной
|
||||
`centralfederal.sqlite` и иметь независимый цикл обновления / бэкапа.
|
||||
- Pipeline сбора — **офлайн-скрипт на cron**, не runtime. На запрос
|
||||
пользователя сервер отдаёт уже собранные данные.
|
||||
- Регион MVP: **ЦФО + Чувашия** (18 субъектов ЦФО + Чувашская
|
||||
Республика, площадь ≈ 670 тыс. км²). Расширение на другие регионы —
|
||||
через конфиг-файл.
|
||||
|
||||
## 3. Scope
|
||||
|
||||
### In scope
|
||||
|
||||
| # | Функция |
|
||||
|------|---------|
|
||||
| F-01 | Поле ввода URL прямой ссылки на GPX в `#sheet-gpx` |
|
||||
| F-02 | Импорт GPX по URL через прокси-эндпоинт `/api/gpx/fetch` |
|
||||
| F-03 | Кнопка «Найти публичные треки» в `#sheet-gpx` — поиск в bbox видимой области карты |
|
||||
| F-04 | Прокси-эндпоинт `/api/gpx/osm/traces` для OSM Public GPS Traces |
|
||||
| F-05 | Список найденных OSM-треков с метаданными (длина, точек, описание, автор) |
|
||||
| F-06 | Импорт выбранного OSM-трека одним тапом |
|
||||
| F-07 | Серверный LRU-кэш ответов внешних API (TTL 24 ч, in-memory) |
|
||||
| F-08 | Источник трека (URL / OSM trace id + ссылка) виден в карточке трека |
|
||||
| F-09 | Лимит размера загруженного по URL файла: 50 МБ (как ET-006) |
|
||||
| F-10 | Внятные сообщения об ошибках (CORS-фейл, 404, лимит API, битый GPX) |
|
||||
| F-11 | Импортированные треки попадают в общий список `window.gpxTracks` и неотличимы от локальных по поведению |
|
||||
| # | Функция |
|
||||
| ----- | ----------------------------------------------------------------------------- |
|
||||
| F-01 | Pipeline сбора GPX-треков с ≥ 3 публичных источников |
|
||||
| F-02 | Хранение треков в SQLite + Spatialite: геометрия + метаданные |
|
||||
| F-03 | Дедупликация: один реальный трек = одна запись, даже если найден в N источниках |
|
||||
| F-04 | Метаданные трека: источник, URL, тип активности, дата, длина, кол-во точек, автор (если публичен) |
|
||||
| F-05 | API endpoint `GET /api/gps-tracks?bbox=…&activity=…&source=…` для отдачи треков клиенту |
|
||||
| F-06 | Векторные тайлы публичных треков `GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt` для эффективной отдачи на низких зумах |
|
||||
| F-07 | Визуализация **отдельными линиями** (не heatmap) на карте |
|
||||
| F-08 | Цветовая дифференциация: палитра по источнику (default) с возможностью переключения на палитру по типу активности |
|
||||
| F-09 | UI-чекбокс «Публичные треки» в `#terrain-popup`: включить/выключить весь слой |
|
||||
| F-10 | UI-фильтр по типу активности (enduro / moto / offroad / bicycle / hike / other), multi-select |
|
||||
| F-11 | UI-фильтр по источнику, multi-select |
|
||||
| F-12 | Конфиг-файл регионов: bbox + название + список активных источников |
|
||||
| F-13 | MVP-датасет: ЦФО + Чувашия, ≥ 5000 треков |
|
||||
| F-14 | Совместимость со сменой стиля карты (через `rebuildMapOverlays()` по аналогии с ET-006 REQ-F-13 и ET-007 REQ-F-06) |
|
||||
| F-15 | Совместимость со спутниковой подложкой (ET-007): треки видны на спутнике с halo для контраста |
|
||||
| F-16 | Клик по треку → popup с метаданными: имя/тип активности/источник/дата/длина и ссылка на оригинал |
|
||||
| F-17 | Health-эндпоинт `/api/gps-tracks/health`: дата последнего сбора, кол-во треков по источникам, ошибки последнего прогона |
|
||||
|
||||
### Out of scope
|
||||
|
||||
- OAuth-интеграции (Strava, Komoot)
|
||||
- Платный API Wikiloc
|
||||
- Поиск треков глобально (без bbox)
|
||||
- Сохранение треков в БД между сессиями
|
||||
- Подписки на пользователей других платформ
|
||||
- Загрузка собственных треков на публичные платформы
|
||||
- **Real-time сбор**: только периодический офлайн (cron, 1–2 раза в неделю).
|
||||
- **Wikiloc Premium / Komoot Premium / любые платные API**: используем
|
||||
только бесплатные публичные endpoints и публичные HTML-страницы там,
|
||||
где это разрешено ToS источника.
|
||||
- **Strava Metro как источник линий**: это heatmap, не отдельные треки —
|
||||
не соответствует бизнес-требованию «отдельные линии». Опционально в
|
||||
будущем — как метрика популярности для валидации, не для MVP.
|
||||
- **OAuth-интеграции** (вход пользователя в Strava/Komoot со своим
|
||||
аккаунтом): отдельный work item.
|
||||
- **Загрузка пользователем своих треков в общую базу**: отдельный work item.
|
||||
- **Редактирование/обрезка треков на стороне сервера**.
|
||||
- **Конвертация из KML/FIT/TCX**: pipeline принимает только GPX.
|
||||
- **Snap-to-road** для треков (выравнивание под дороги OSM).
|
||||
- **Учёт сложности (drag-level) внутри трека**: фильтр только по типу
|
||||
активности; сложность — отдельная задача (требует анализа геометрии и
|
||||
скорости).
|
||||
|
||||
## 4. Метрики успеха
|
||||
## 4. Источники (с оценкой реализуемости в MVP)
|
||||
|
||||
| Метрика | Критерий |
|
||||
|---------|----------|
|
||||
| URL-импорт | Прямая ссылка на GPX до 50 МБ загружается за ≤ 5 сек на средней сети |
|
||||
| OSM-поиск bbox | Запрос видимой области возвращает результат за ≤ 3 сек (с кэшем — мгновенно) |
|
||||
| Точность | OSM-трек после импорта визуально совпадает с тем же треком из osm.org |
|
||||
| Кэш | Повторный запрос той же области/URL в течение 24 ч — без обращения к внешнему API |
|
||||
| UX | Все ошибки (CORS, 404, лимит, формат) — внятные toast-уведомления, не падение |
|
||||
| Совместимость с ET-006 | Локальные и удалённые треки в одном списке, поведение идентично |
|
||||
| Сохранение при смене стиля | Импортированные треки переживают переключение тёмной темы и слоёв рельефа |
|
||||
Анализ каждого источника из business request с честной оценкой
|
||||
доступности и юридических условий:
|
||||
|
||||
## 5. Риски
|
||||
| # | Источник | Тип доступа | MVP | Комментарий |
|
||||
| - | ------------------------- | ------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | OSM Public GPS Traces | Документированный API | **да** | `/api/0.6/trackpoints?bbox=…&page=…`. Лицензия ODbL, атрибуция OSM. Объём для ЦФО оценочно ≈ 50–100K точек, тыс. треков. |
|
||||
| 2 | EnduroRussia.ru | Web (HTML + GPX-ссылки) | **да** | По регионам, есть прямые GPX-ссылки. Лицензия и условия скрейпинга — фиксируются в ADR `06-adr/source-licensing.md` до начала разработки. |
|
||||
| 3 | Тропинки.ру / ttrails.ru | Web (GPX/KML) | **да** | Эндуро-категория, GPX доступен без авторизации. Условия скрейпинга — то же ADR. |
|
||||
| 4 | Offmaps.ru | Web | пилот | Требует ревью формата выдачи и лицензии. Подключаем в пилот-режим если ADR разрешает. |
|
||||
| 5 | Nakarte.me | Public layers + JSON | пилот | Агрегатор: содержит ссылки на треки внешних источников. Может быть «бесплатным» путём к Wikiloc/Strava-treki косвенно. Требует ревью лицензии. |
|
||||
| 6 | Wikiloc | API (премиум) | нет | Бесплатный публичный API не отдаёт GPX. Без премиума — невозможно. **Откладываем.** |
|
||||
| 7 | Komoot | API (партнёрский) | нет | Публичный API ограничен, нет публичной выдачи GPX по bbox. **Откладываем.** |
|
||||
| 8 | Strava Metro | API (исследовательский) | нет | Heatmap, не отдельные треки → не соответствует бизнес-требованию. **Out of scope.** |
|
||||
|
||||
| Риск | Вероятность | Влияние | Митигация |
|
||||
|------|-------------|---------|-----------|
|
||||
| OSM API rate limit (1 запрос / IP / сек) | Высокая | Среднее | Серверный кэш по bbox + дебаунс на клиенте |
|
||||
| URL-прокси превращается в open redirect / SSRF | Средняя | Высокое | Whitelist схем (http/https), блок приватных IP, лимит размера, таймаут |
|
||||
| Большие OSM-страницы (1000+ треков) → длинный список | Средняя | Низкое | Пагинация: показывать первые N, кнопка «ещё» |
|
||||
| GPX по URL не существует / 404 | Высокая | Низкое | Toast с понятной ошибкой |
|
||||
| Content-Type не `application/gpx+xml` | Высокая | Низкое | Проверять по содержимому (DOMParser), не по заголовкам |
|
||||
| Чужой публичный трек содержит вредоносный XML / XXE | Низкая | Высокое | DOMParser в браузере (XXE отключён), на бэкенде — `defusedxml` |
|
||||
| Внешний API внезапно недоступен | Средняя | Низкое | Graceful degradation: показать сообщение, не блокировать другие функции |
|
||||
**MVP-минимум: 3 источника живут в продакшне** — обязательно OSM
|
||||
(гарантированно доступен), плюс минимум 2 из (2)–(5) по результатам
|
||||
ADR-ревью лицензий.
|
||||
|
||||
## 6. Зависимости
|
||||
### Юридический минимум
|
||||
|
||||
- **ET-006** — модель `window.gpxTracks`, рендеринг, sheet `#sheet-gpx`,
|
||||
парсер `parseGpx()`. Без ET-006 эта задача не имеет смысла.
|
||||
- **Backend (FastAPI)** — новые эндпоинты `/api/gpx/fetch`,
|
||||
`/api/gpx/osm/traces`, добавление `httpx` (уже есть) и `defusedxml`
|
||||
(новая зависимость, опционально — для server-side валидации).
|
||||
- Внешние сервисы:
|
||||
- `https://api.openstreetmap.org/api/0.6/trackpoints` — публичный API
|
||||
OSM, ограничения: 1 req/sec/IP, 5000 точек/страница, до 5 страниц.
|
||||
- Произвольные HTTPS-хосты (для URL-импорта) — без SLA, fail-soft.
|
||||
Перед началом разработки каждого источника (2)–(5) — **обязательный
|
||||
ADR** `docs/work-items/ET-008/06-adr/<source>-licensing.md`:
|
||||
|
||||
1. Что говорит ToS источника о скрейпинге / массовой загрузке GPX.
|
||||
2. Что говорит robots.txt.
|
||||
3. На каких условиях разрешена публикация чужих треков
|
||||
(имя/анонимизация/атрибуция).
|
||||
4. Rate-limit, который мы будем соблюдать (default: 1 req / 5 sec, с
|
||||
корректным `User-Agent: enduro-trails/<v> (+contact)`).
|
||||
5. Список метаданных, которые **нельзя** сохранять/публиковать (личные
|
||||
адреса, имена при отсутствии явного согласия).
|
||||
|
||||
Источник без явного зелёного света в ADR — **не включается** в pipeline.
|
||||
|
||||
## 5. Метрики успеха
|
||||
|
||||
| Метрика | Критерий MVP |
|
||||
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
|
||||
| Покрытие региона | ≥ 5000 уникальных треков для ЦФО + Чувашии после первого полного прогона pipeline |
|
||||
| Источники в продакшне | ≥ 3 источника, отдающих данные в БД |
|
||||
| Дедупликация | < 5% дублей (один реальный трек — одна запись). Метрика: руками отсэмплировать 100 треков, посчитать дубли. |
|
||||
| Производительность отдачи bbox | `GET /api/gps-tracks?bbox=…` ≤ 300 мс p95 на z ≥ 10 (≤ 500 треков в видимой области) |
|
||||
| Производительность отдачи тайлов | `GET /api/gps-tracks/tiles/{z}/{x}/{y}.mvt` ≤ 200 мс p95 на z = 8–11 |
|
||||
| Производительность отрисовки | При включённом слое pan/zoom без видимых фризов на десктопе и мобильных с 4 ГБ RAM |
|
||||
| Расширяемость на регион | Добавить новый регион (bbox + название + список источников) — ≤ 30 строк YAML-конфига, без правки кода |
|
||||
| Скорость UI-фильтров | Переключение фильтра по активности/источнику меняет видимую выборку за ≤ 200 мс (фильтрация на клиенте) |
|
||||
| Сохранение слоя при `setStyle()` | Слой не теряется при переключении тёмной темы / спутника / hillshade — восстанавливается через `rebuildMapOverlays()` |
|
||||
| Pipeline стабильность | Падение парсера одного источника не валит остальных; лог + алерт в `/api/gps-tracks/health` |
|
||||
| Атрибуция | На карте видна атрибуция каждого активного источника; в popup трека — ссылка на оригинал |
|
||||
|
||||
## 6. Риски
|
||||
|
||||
| Риск | Вероятность | Влияние | Митигация |
|
||||
| ----------------------------------------------------------------------------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Источник меняет HTML → парсер ломается | Высокая | Среднее | Каждый источник в отдельном модуле, изолированная ошибка. Pipeline пишет статус по источнику в health-эндпоинт. Алерт при 2 неудачных прогонах подряд. |
|
||||
| ToS источника запрещает скрейпинг | Средняя | Высокое | Обязательный ADR с фиксацией лицензии до подключения источника. Источник без явного разрешения — не включается. |
|
||||
| Дубли треков из разных источников (один и тот же трек выкладывают на 2 платформах) | Высокая | Среднее | Spatial+temporal hash: bbox округлённый до 0.01° + длина ± 5% + дата ± 1 день → одна запись. Алгоритм в TRZ §6. |
|
||||
| Перегрузка карты на низких зумах (10K+ треков в видимой области) | Высокая | Высокое | На клиенте: на z < 10 — отдача через MVT-тайлы с упрощением геометрии (как `simplify_coords` для `trails`). На z ≥ 10 — JSON с лимитом 500 треков. |
|
||||
| Размер БД растёт неконтролируемо (миллионы треков при расширении на РФ) | Низкая | Среднее | Отдельная `gps_tracks.sqlite`. Ротация: треки старше N лет (по конфигу, default 5) удаляются. Метрика размера БД в health. |
|
||||
| Скрейпер банится по IP | Средняя | Среднее | Rate-limit + backoff + `User-Agent` с контактом. Сбор по cron 1–2 раза в неделю, не чаще. Per-source конфигурируемый delay. |
|
||||
| Персональные данные в треках (точки «дом», имена) | Низкая | Высокое | Не сохраняем waypoint без явного публичного флага. Не сохраняем `author` если ToS требует анонимизации. Список запрещённых полей — в `08-data-requirements.md`. |
|
||||
| Лицензия источника обязывает менять/удалять данные по требованию автора | Средняя | Среднее | Сохраняем `external_id` и `external_url` — можем удалить точечно по запросу. Pipeline уважает «удалённое на источнике» → удалять и у нас. |
|
||||
| Pipeline ест слишком много трафика mva154 | Средняя | Низкое | Per-source лимит на прогон (например, max 1000 новых треков за прогон). Метрики в health. |
|
||||
| Отдача больших MVT тайлов медленная | Средняя | Среднее | Серверный кэш тайлов (LRU 1024 записи, как уже сделано для `trails`). Упрощение геометрии по зуму. |
|
||||
|
||||
## 7. Зависимости
|
||||
|
||||
### Backend
|
||||
|
||||
- Новый пакет `src/api/gps_tracks/` с подмодулями:
|
||||
- `models.py` — Pydantic + SQL schema
|
||||
- `sources/<source>.py` — модули per-source (OSM, EnduroRussia, ttrails, …)
|
||||
- `dedup.py` — алгоритм дедупликации
|
||||
- `db.py` — обвязка SQLite + Spatialite
|
||||
- `endpoint.py` — FastAPI routes
|
||||
- `mvt.py` — генерация MVT-тайлов
|
||||
- Зависимости Python: `httpx` (есть), `lxml` или `defusedxml` (новая —
|
||||
для безопасного парсинга XML на сервере), `shapely` (есть).
|
||||
|
||||
### Pipeline
|
||||
|
||||
- Скрипт `scripts/gps_collect.py` — точка входа.
|
||||
- Конфиг `config/gps_sources.yaml` — список источников и параметры.
|
||||
- Конфиг `config/gps_regions.yaml` — список регионов (bbox + список
|
||||
активных источников per-region).
|
||||
- Cron на mva154: `0 3 * * 1,4 /usr/local/bin/python
|
||||
/opt/enduro-trails/scripts/gps_collect.py` (Mon + Thu, 03:00 UTC).
|
||||
- Логи: `/var/log/enduro-trails/gps-collect.log`.
|
||||
|
||||
### Frontend
|
||||
|
||||
- Новый модуль `src/web/gps_tracks.js` — слой, фильтры, popup, по
|
||||
аналогии с `gpx.js`.
|
||||
- Расширение `index.html`:
|
||||
- Чекбокс «Публичные треки» и кнопка «Фильтры» в `#terrain-popup`.
|
||||
- Bottom sheet `#sheet-gps-filters` с фильтрами по активности и
|
||||
источнику.
|
||||
- Расширение `style.json` / `style-dark.json`: layer + halo-layer для
|
||||
спутника (по аналогии с `trails-track-halo-satellite` из ET-007).
|
||||
- Интеграция с `rebuildMapOverlays()` в `app.js`.
|
||||
|
||||
### Инфра
|
||||
|
||||
- Файловая: `data/gps_tracks.sqlite` на mva154, права чтения для FastAPI,
|
||||
права записи только для pipeline. Бэкап в общий backup-стек проекта.
|
||||
- Сетевая: исходящие HTTPS к источникам с mva154 (уже разрешено).
|
||||
|
||||
### Документация
|
||||
|
||||
- `06-adr/source-licensing.md` — лицензии всех источников.
|
||||
- `06-adr/dedup-algorithm.md` — обоснование выбора алгоритма
|
||||
дедупликации.
|
||||
- `06-adr/storage-schema.md` — обоснование отдельной БД vs единой.
|
||||
- `07-infra-requirements.md` — cron, бэкапы, ротация, мониторинг.
|
||||
- `08-data-requirements.md` — схема БД, поля, ограничения, политика
|
||||
персональных данных.
|
||||
- `10-tech-risks.md` — расширенный риск-реестр (расширяет §6 BRD).
|
||||
|
||||
### Связи с другими work items
|
||||
|
||||
- **ET-006** — модель `window.gpxTracks` живёт параллельно. ET-008 не
|
||||
трогает её, использует свою модель `window.gpsTracksLayer`.
|
||||
- **ET-007** — спутниковая подложка. ET-008 добавляет halo-слой для
|
||||
публичных треков в режиме «Спутник» по тому же паттерну.
|
||||
- **PH-3 Smart Route** — публичные треки в будущем могут стать входом
|
||||
для построения «реально-езженого» маршрута. Не в scope ET-008.
|
||||
- **PH-9 PWA** — слой публичных треков должен корректно работать в
|
||||
офлайне (через cached MVT). Учитывается в TRZ, но реализация офлайна
|
||||
— задача PH-9.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,274 +2,442 @@
|
||||
type: acceptance-criteria
|
||||
work_item_id: ET-008
|
||||
title: "AC: GPS-треки с публичных платформ на карте"
|
||||
version: 1
|
||||
version: 2
|
||||
status: draft
|
||||
created_at: 2026-06-01
|
||||
updated_at: 2026-06-01
|
||||
changelog:
|
||||
- "v2 (2026-06-01): полная переработка под BRD/TRZ v2 — критерии серверной агрегации, дедупликации, MVT-тайлов, фильтров активности/источника, popup, halo-на-спутнике. Предыдущая v1 описывала URL-импорт + OSM live-поиск."
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
---
|
||||
|
||||
# Acceptance Criteria — ET-008: GPS-треки с публичных платформ на карте
|
||||
|
||||
## AC-01: Секция «Источники» в `#sheet-gpx`
|
||||
## AC-01: Конфигурация источников и регионов
|
||||
|
||||
```gherkin
|
||||
Feature: Переключатель источников треков
|
||||
Feature: Расширяемая конфигурация
|
||||
|
||||
Scenario: Открытие GPX-панели
|
||||
Given пользователь нажимает кнопку GPX в нижнем тулбаре
|
||||
Then открывается панель #sheet-gpx
|
||||
And в верхней части видна секция «Источники» с тремя кнопками: «Из файла», «По ссылке», «Найти рядом»
|
||||
And по умолчанию активна кнопка «Из файла»
|
||||
Scenario: Включение нового источника
|
||||
Given config/gps_sources.yaml содержит источник с enabled=false
|
||||
When оператор меняет на enabled=true и перезапускает pipeline
|
||||
Then источник участвует в следующем прогоне
|
||||
And в /api/gps-tracks/health он появляется в tracks_by_source
|
||||
|
||||
Scenario: Переключение на «По ссылке»
|
||||
Given панель #sheet-gpx открыта
|
||||
When пользователь нажимает кнопку «По ссылке»
|
||||
Then кнопка «По ссылке» становится активной
|
||||
And отображается поле ввода URL и кнопка «Загрузить»
|
||||
And контент других вкладок скрыт
|
||||
Scenario: Добавление нового региона
|
||||
Given оператор добавляет в config/gps_regions.yaml новую запись с bbox
|
||||
And запись не превышает 30 строк YAML
|
||||
When оператор запускает pipeline без аргументов
|
||||
Then новый регион обрабатывается всеми указанными в нём источниками
|
||||
And никаких правок Python-кода не требуется
|
||||
|
||||
Scenario: Переключение на «Найти рядом»
|
||||
Given панель #sheet-gpx открыта
|
||||
When пользователь нажимает кнопку «Найти рядом»
|
||||
Then отображается кнопка «Найти треки в этой области карты»
|
||||
Scenario: Отключение источника
|
||||
Given источник был enabled=true и собрал N треков
|
||||
When оператор меняет на enabled=false
|
||||
Then следующий прогон pipeline пропускает этот источник
|
||||
And ранее собранные треки остаются в БД и отдаются API
|
||||
And в фильтре по источнику соответствующий чекбокс не выбран по умолчанию
|
||||
```
|
||||
|
||||
## AC-02: Импорт по URL — успешный сценарий
|
||||
## AC-02: Pipeline сбора
|
||||
|
||||
```gherkin
|
||||
Feature: Загрузка GPX по прямой ссылке
|
||||
Feature: Pipeline gps_collect.py
|
||||
|
||||
Scenario: Валидная публичная ссылка
|
||||
Given активна вкладка «По ссылке»
|
||||
When пользователь вставляет https://example.com/test-track.gpx (валидный, 1 МБ)
|
||||
And нажимает «Загрузить»
|
||||
Then показывается индикатор загрузки
|
||||
And через ≤ 5 сек трек появляется на карте
|
||||
And карта выполняет fit bounds
|
||||
And трек добавляется в список #gpx-list
|
||||
And в карточке трека отображается «🔗 example.com»
|
||||
Scenario: Полный прогон по умолчанию
|
||||
Given config содержит регион ЦФО+Чувашия и 3 source enabled
|
||||
When оператор запускает scripts/gps_collect.py
|
||||
Then pipeline проходит по всем регионам и всем enabled-источникам
|
||||
And для каждой пары (region, source) пишется запись в pipeline_runs
|
||||
And exit code == 0 если хотя бы один трек собран по каждому источнику
|
||||
|
||||
Scenario: Загрузка по Enter
|
||||
Given активна вкладка «По ссылке»
|
||||
When пользователь вставляет URL и нажимает Enter
|
||||
Then загрузка начинается без клика по кнопке
|
||||
Scenario: Прогон одного источника
|
||||
When оператор запускает scripts/gps_collect.py --source osm
|
||||
Then обрабатывается только OSM
|
||||
And остальные source пропускаются
|
||||
|
||||
Scenario: Падение одного источника не валит остальные
|
||||
Given OSM возвращает 503 на весь прогон
|
||||
When pipeline запущен
|
||||
Then OSM-источник помечается status='error' в pipeline_runs
|
||||
And другие источники продолжают работу
|
||||
And exit code сигнализирует ошибку (1) если запрошен strict-mode, иначе 0
|
||||
|
||||
Scenario: Dry-run
|
||||
When оператор запускает с --dry-run
|
||||
Then никаких INSERT в БД не делается
|
||||
And pipeline_runs тоже не пишется
|
||||
And в stdout выводится план: N треков было бы собрано
|
||||
|
||||
Scenario: Уважение rate-limit
|
||||
Given у источника rate_limit_sec=5
|
||||
When pipeline делает 10 последовательных запросов к этому источнику
|
||||
Then суммарное время ≥ 9 * 5 = 45 сек (между запросами)
|
||||
```
|
||||
|
||||
## AC-03: Импорт по URL — ошибки
|
||||
## AC-03: Дедупликация
|
||||
|
||||
```gherkin
|
||||
Feature: Обработка ошибок URL-импорта
|
||||
Feature: Дедупликация треков
|
||||
|
||||
Scenario: Невалидный URL (схема)
|
||||
Given активна вкладка «По ссылке»
|
||||
When пользователь вставляет ftp://example.com/file.gpx
|
||||
Then показывается toast «Невалидная ссылка»
|
||||
And запрос на бэкенд не отправляется
|
||||
Scenario: Один трек найден в двух источниках
|
||||
Given OSM и EnduroRussia отдали один и тот же трек
|
||||
(один автор выложил на обоих)
|
||||
And bbox и длина совпадают в пределах допуска
|
||||
And даты совпадают
|
||||
When pipeline обрабатывает обе записи
|
||||
Then в БД одна запись tracks
|
||||
And sources_json содержит обоих
|
||||
And external_urls_json содержит обе ссылки
|
||||
|
||||
Scenario: Приватный IP
|
||||
Given пользователь вставляет http://192.168.1.1/file.gpx
|
||||
Then бэкенд возвращает 400
|
||||
And показывается toast «Эта ссылка недоступна»
|
||||
Scenario: Похожие треки разных дат — НЕ дубли
|
||||
Given два трека с одинаковым bbox и длиной
|
||||
And даты отличаются на > 1 день
|
||||
Then записи разные, дедуп НЕ срабатывает
|
||||
|
||||
Scenario: Несуществующий файл
|
||||
Given URL ведёт на 404
|
||||
Then показывается toast «Файл не найден по этой ссылке»
|
||||
Scenario: Треки без даты от разных источников
|
||||
Given оба трека без created_at
|
||||
And bbox и длина совпадают
|
||||
Then дедуп срабатывает (по умолчанию консервативный merge)
|
||||
And это поведение задокументировано в ADR-002
|
||||
|
||||
Scenario: Файл больше 50 МБ
|
||||
Given URL ведёт на GPX > 50 МБ
|
||||
Then показывается toast «Файл слишком большой (макс. 50 МБ)»
|
||||
|
||||
Scenario: Не GPX (HTML по ссылке)
|
||||
Given URL отдаёт HTML-страницу
|
||||
Then показывается toast «По этой ссылке не GPX-файл»
|
||||
|
||||
Scenario: Внешний сервер не отвечает
|
||||
Given внешний сервер таймаутит
|
||||
Then показывается toast «Сервер не отвечает, попробуйте позже»
|
||||
Scenario: Метрика < 5% дубликатов
|
||||
Given в БД собрано ≥ 5000 треков
|
||||
When QA-инженер выбирает 100 случайных треков и руками проверяет дубли
|
||||
Then не более 5 треков (5%) являются дублями
|
||||
```
|
||||
|
||||
## AC-04: Поиск OSM-треков
|
||||
## AC-04: Endpoint /api/gps-tracks (GeoJSON)
|
||||
|
||||
```gherkin
|
||||
Feature: Поиск публичных треков OSM в видимой области
|
||||
Feature: GeoJSON endpoint
|
||||
|
||||
Scenario: Успешный поиск с результатами
|
||||
Given активна вкладка «Найти рядом»
|
||||
And карта показывает область с публичными треками
|
||||
When пользователь нажимает «Найти треки в этой области карты»
|
||||
Then показывается индикатор загрузки
|
||||
And через ≤ 3 сек появляется список найденных треков
|
||||
And каждая карточка содержит: иконку OSM, описание (page N), длину в км, кнопку «Показать», ссылку «↗»
|
||||
Scenario: Запрос с малым bbox
|
||||
Given в БД 1000 треков, из них 50 в bbox=[37.5,55.6,37.7,55.8]
|
||||
When клиент шлёт GET /api/gps-tracks?bbox=37.5,55.6,37.7,55.8
|
||||
Then ответ 200, FeatureCollection с 50 features
|
||||
And total_in_bbox=50, returned=50, truncated=false
|
||||
And time ≤ 300 мс p95
|
||||
|
||||
Scenario: Пустая область
|
||||
Given карта показывает область без публичных треков
|
||||
When пользователь нажимает «Найти треки»
|
||||
Then отображается inline-сообщение «В этой области нет публичных GPS-треков»
|
||||
Scenario: Bbox с обрезкой по limit
|
||||
Given в bbox 1500 треков
|
||||
When клиент шлёт GET .../api/gps-tracks?bbox=...&limit=500
|
||||
Then returned=500, total_in_bbox=1500, truncated=true
|
||||
|
||||
Scenario: Слишком большая область
|
||||
Given карта показывает область с bbox > 0.25 deg²
|
||||
When пользователь нажимает «Найти треки»
|
||||
Then показывается toast «Слишком большая область, увеличьте zoom»
|
||||
And запрос на бэкенд не отправляется (или возвращается 400)
|
||||
Scenario: Фильтр по активности
|
||||
Given в bbox 100 треков, 20 enduro, 30 moto, 50 hike
|
||||
When клиент шлёт ?activity=enduro,moto
|
||||
Then returned=50
|
||||
|
||||
Scenario: Пагинация
|
||||
Given поиск вернул has_more=true
|
||||
Then в конце списка отображается кнопка «Показать ещё»
|
||||
When пользователь нажимает «Показать ещё»
|
||||
Then дозагружаются результаты следующей страницы
|
||||
And они добавляются в конец списка
|
||||
Scenario: Фильтр по источнику
|
||||
Given в bbox 100 треков: 60 OSM, 30 EnduroRussia, 10 ttrails
|
||||
When клиент шлёт ?source=osm
|
||||
Then returned=60
|
||||
|
||||
Scenario: Невалидный bbox
|
||||
When клиент шлёт bbox=foo
|
||||
Then ответ 400
|
||||
|
||||
Scenario: bbox вне диапазона координат
|
||||
When клиент шлёт bbox=200,100,250,150
|
||||
Then ответ 400
|
||||
|
||||
Scenario: Поля feature.properties
|
||||
Then каждая feature содержит: name, activity_type, user, created_at,
|
||||
length_km, sources (array), external_urls (array)
|
||||
```
|
||||
|
||||
## AC-05: Импорт OSM-трека
|
||||
## AC-05: Endpoint /api/gps-tracks/tiles MVT
|
||||
|
||||
```gherkin
|
||||
Feature: Импорт выбранного OSM-трека на карту
|
||||
Feature: MVT tiles
|
||||
|
||||
Scenario: Импорт по кнопке «Показать»
|
||||
Given найдено 3 OSM-трека в списке
|
||||
When пользователь нажимает «Показать» у первого трека
|
||||
Then показывается индикатор загрузки
|
||||
And через ≤ 5 сек трек появляется на карте
|
||||
And карта выполняет fit bounds
|
||||
And трек добавляется в #gpx-list
|
||||
And в карточке трека отображается «🌍 OSM #...» (кликабельная ссылка)
|
||||
And карточка в #gpx-nearby-results получает индикатор «✓ Загружен»
|
||||
Scenario: Отдача тайла на z=10
|
||||
Given в БД есть треки в видимой области
|
||||
When клиент шлёт GET /api/gps-tracks/tiles/10/623/325.mvt
|
||||
Then ответ 200, Content-Type: application/x-protobuf
|
||||
And тело содержит layer gps_tracks с LineString features
|
||||
|
||||
Scenario: Повторный импорт того же трека
|
||||
Given OSM-трек уже импортирован
|
||||
When пользователь нажимает «Показать» у этой же карточки в найденных
|
||||
Then показывается toast «Уже загружен»
|
||||
And новый трек НЕ добавляется
|
||||
Scenario: Тайл из кэша
|
||||
Given тайл уже запрашивали
|
||||
When повторный запрос того же z/x/y
|
||||
Then header X-Cache: HIT
|
||||
And время ≤ 20 мс p95
|
||||
|
||||
Scenario: Внешняя ссылка на osm.org
|
||||
Given в карточке найденного трека есть кнопка «↗»
|
||||
When пользователь нажимает «↗»
|
||||
Then открывается новая вкладка с страницей трека на openstreetmap.org
|
||||
Scenario: Упрощение геометрии на низких зумах
|
||||
Given исходный трек 1000 точек на z=7
|
||||
When MVT генерируется
|
||||
Then feature имеет упрощённую геометрию (≤ 100 точек после Douglas-Peucker)
|
||||
|
||||
Scenario: Properties фичи в MVT
|
||||
Then feature.properties содержит: id, activity, source, sources,
|
||||
length_km, name, ext_url
|
||||
```
|
||||
|
||||
## AC-06: Отображение источника в карточке трека
|
||||
## AC-06: Endpoint health
|
||||
|
||||
```gherkin
|
||||
Feature: Источник трека виден пользователю
|
||||
Feature: Health endpoint
|
||||
|
||||
Scenario: Локальный файл (ET-006 совместимость)
|
||||
Given загружен GPX из локального файла
|
||||
Then в карточке трека под именем файла отображается «📁 локальный файл»
|
||||
Scenario: Полный отчёт
|
||||
When клиент шлёт GET /api/gps-tracks/health
|
||||
Then ответ 200 JSON содержит:
|
||||
| db_path |
|
||||
| db_size_mb |
|
||||
| tracks_total |
|
||||
| tracks_by_source | (объект source_id → int)
|
||||
| tracks_by_activity | (объект activity → int)
|
||||
| last_pipeline_run | (объект с started/finished/sources_ok/sources_error)
|
||||
| tile_cache_size |
|
||||
|
||||
Scenario: Загружен по URL
|
||||
Given загружен GPX по ссылке https://github.com/user/repo/track.gpx
|
||||
Then в карточке трека отображается «🔗 github.com»
|
||||
|
||||
Scenario: Загружен из OSM
|
||||
Given загружен OSM-трек page 0
|
||||
Then в карточке трека отображается ссылка «🌍 OSM #..» которая ведёт на osm.org
|
||||
Scenario: Health без БД
|
||||
Given БД отсутствует на диске
|
||||
When клиент шлёт GET /api/gps-tracks/health
|
||||
Then ответ содержит tracks_total=0 и предупреждение о БД (или 503)
|
||||
```
|
||||
|
||||
## AC-07: Кэширование на бэкенде
|
||||
## AC-07: Чекбокс «Публичные треки» в попапе
|
||||
|
||||
```gherkin
|
||||
Feature: Серверный кэш ответов внешних API
|
||||
Feature: Включение слоя из попапа
|
||||
|
||||
Scenario: Повторный запрос URL из кэша
|
||||
Given URL запрашивался менее 24 часов назад
|
||||
When клиент делает повторный GET /api/gpx/fetch?url=...
|
||||
Then ответ возвращается с заголовком X-Cache: HIT
|
||||
And время ответа ≤ 50 мс
|
||||
And внешний запрос НЕ выполняется
|
||||
Scenario: Чекбокс присутствует
|
||||
Given пользователь нажимает #terrain-toggle
|
||||
Then в попапе #terrain-popup видна строка «Публичные треки» с чекбоксом
|
||||
|
||||
Scenario: Cache miss
|
||||
Given URL запрашивается впервые
|
||||
Then выполняется внешний запрос
|
||||
And ответ возвращается с X-Cache: MISS
|
||||
And следующий запрос того же URL — HIT
|
||||
Scenario: Включение слоя
|
||||
When пользователь ставит галку «Публичные треки»
|
||||
Then на карте появляются линии треков
|
||||
And localStorage['gps-tracks-enabled'] = 'true'
|
||||
And рядом с чекбоксом появляется ссылка «Фильтры…»
|
||||
|
||||
Scenario: Повторный bbox-поиск из кэша
|
||||
Given bbox запрашивался менее 24 часов назад
|
||||
When клиент делает повторный GET /api/gpx/osm/traces?bbox=...
|
||||
Then ответ из кэша
|
||||
And внешний запрос к OSM API НЕ выполняется
|
||||
Scenario: Выключение слоя
|
||||
When пользователь снимает галку
|
||||
Then линии исчезают с карты
|
||||
And localStorage = 'false'
|
||||
And ссылка «Фильтры…» скрывается
|
||||
|
||||
Scenario: Подсказка о минимальном zoom
|
||||
Given текущий zoom < 8
|
||||
And чекбокс включён
|
||||
Then рядом с чекбоксом видна подсказка «Зум 8+»
|
||||
And линии на карте не видны (без ошибок)
|
||||
```
|
||||
|
||||
## AC-08: Безопасность
|
||||
## AC-08: Фильтры по активности и источнику
|
||||
|
||||
```gherkin
|
||||
Feature: SSRF protection
|
||||
Feature: Sheet фильтров
|
||||
|
||||
Scenario: Прямой запрос к loopback
|
||||
When клиент шлёт GET /api/gpx/fetch?url=http://127.0.0.1/data
|
||||
Then бэкенд возвращает 400
|
||||
And никакого запроса к 127.0.0.1 не делается
|
||||
Scenario: Открытие sheet
|
||||
Given слой включён
|
||||
When пользователь нажимает «Фильтры…»
|
||||
Then открывается #sheet-gps-filters
|
||||
And видны секции «Тип активности», «Источник», «Цвет линий»
|
||||
And по умолчанию выбраны все активности и все источники
|
||||
|
||||
Scenario: Запрос к приватной подсети
|
||||
When клиент шлёт URL ведущий на 10.0.0.1, 192.168.x.x, 172.16.x.x
|
||||
Then бэкенд возвращает 400
|
||||
Scenario: Фильтрация по активности
|
||||
Given в видимой области карты 743 трека, 200 enduro, 50 moto, …
|
||||
When пользователь снимает все галки кроме «Эндуро» и «Мото»
|
||||
Then на карте отображаются только enduro и moto треки
|
||||
And gps-stat-shown отражает новое число
|
||||
And фильтрация мгновенная (≤ 200 мс), без сетевого запроса
|
||||
|
||||
Scenario: Редирект на приватный IP
|
||||
Given внешний URL отдаёт 302 redirect на http://127.0.0.1/...
|
||||
When клиент шлёт GET /api/gpx/fetch?url=<external>
|
||||
Then редирект проверяется повторно и блокируется
|
||||
And бэкенд возвращает 400
|
||||
Scenario: Фильтрация по источнику
|
||||
Given включено 3 источника
|
||||
When пользователь снимает «OSM»
|
||||
Then OSM-треки скрываются на карте
|
||||
|
||||
Scenario: Запрещённая схема
|
||||
When клиент шлёт URL с file:// или gopher://
|
||||
Then бэкенд возвращает 400
|
||||
Scenario: Переключение режима цвета
|
||||
Given color-mode = 'source'
|
||||
When пользователь выбирает «По активности»
|
||||
Then цвета линий перерисовываются по палитре активности
|
||||
And localStorage сохраняет 'gps-tracks-color-mode' = 'activity'
|
||||
|
||||
Scenario: Размер ответа превышает лимит
|
||||
Given внешний сервер начинает стримить файл > 50 МБ
|
||||
Then бэкенд прерывает соединение
|
||||
And возвращает 413
|
||||
Scenario: Сохранение фильтров между сессиями
|
||||
Given пользователь настроил фильтры (только enduro, только OSM)
|
||||
When пользователь перезагружает страницу
|
||||
Then sheet-фильтров восстанавливает те же чекбоксы
|
||||
And слой отображает только enduro+OSM треки
|
||||
```
|
||||
|
||||
## AC-09: Совместимость с ET-006
|
||||
## AC-09: Popup при клике на трек
|
||||
|
||||
```gherkin
|
||||
Feature: Локальные и удалённые треки в одной модели
|
||||
Feature: Popup трека
|
||||
|
||||
Scenario: Смешанный список
|
||||
Given загружен 1 локальный файл, 1 по URL, 1 из OSM
|
||||
Then в #gpx-list отображаются 3 карточки
|
||||
And каждая имеет уникальный цвет из палитры
|
||||
And каждая имеет свой индикатор источника
|
||||
And любую можно активировать, удалить, увидеть профиль высот
|
||||
Scenario: Клик по линии трека
|
||||
Given на карте отображается слой публичных треков
|
||||
When пользователь кликает на линию трека
|
||||
Then открывается popup с полями: name, activity (иконка+текст),
|
||||
length_km, points_count, created_at, user, sources (со ссылками)
|
||||
|
||||
Scenario: Сохранение при смене темы
|
||||
Given на карте 3 трека разных источников
|
||||
When пользователь переключает тёмную тему
|
||||
Then все 3 трека остаются на карте
|
||||
And источники в карточках сохраняются
|
||||
And статистика и профиль активного трека сохраняются
|
||||
Scenario: Трек из двух источников
|
||||
Given трек имеет sources=['osm', 'enduro_russia']
|
||||
Then popup показывает обе ссылки
|
||||
|
||||
Scenario: Сохранение при переключении слоёв рельефа
|
||||
Given на карте 3 трека разных источников
|
||||
Scenario: Трек без user/name
|
||||
Then popup показывает «Без названия» и не показывает строку «Автор»
|
||||
|
||||
Scenario: Клик по фону карты
|
||||
Given открыт popup
|
||||
When пользователь кликает на пустое место карты
|
||||
Then popup закрывается
|
||||
```
|
||||
|
||||
## AC-10: Z-order и совместимость с другими слоями
|
||||
|
||||
```gherkin
|
||||
Feature: Z-order
|
||||
|
||||
Scenario: Слой выше trails, ниже маршрута OSRM
|
||||
Given на карте: OSM tiles + trails + публичные треки + маршрут OSRM
|
||||
Then визуально маршрут OSRM перекрывает публичные треки
|
||||
And публичные треки перекрывают trails из vector tiles
|
||||
And базовая карта (OSM) — самый нижний
|
||||
|
||||
Scenario: Совместимость с ET-006 (личные GPX)
|
||||
Given пользователь загрузил свой GPX-файл (ET-006)
|
||||
And слой публичных треков включён
|
||||
Then оба видны параллельно
|
||||
And личный трек визуально выше публичных
|
||||
```
|
||||
|
||||
## AC-11: Совместимость со спутниковой подложкой (ET-007)
|
||||
|
||||
```gherkin
|
||||
Feature: Halo на спутнике
|
||||
|
||||
Scenario: Включение спутника
|
||||
Given слой публичных треков включён
|
||||
When пользователь переключает подложку на «Спутник»
|
||||
Then линии треков видны на спутнике
|
||||
And появляется белая обводка (halo) для контраста
|
||||
|
||||
Scenario: Возврат на схему
|
||||
When пользователь возвращается на «Схема»
|
||||
Then halo скрывается
|
||||
And линии отображаются обычными цветами
|
||||
|
||||
Scenario: Halo учитывает чекбокс
|
||||
Given спутник активен
|
||||
When пользователь выключает чекбокс «Публичные треки»
|
||||
Then и линии, и halo скрываются
|
||||
```
|
||||
|
||||
## AC-12: Сохранение при смене стиля карты
|
||||
|
||||
```gherkin
|
||||
Feature: Переживание setStyle()
|
||||
|
||||
Scenario: Переключение тёмной темы
|
||||
Given слой включён, фильтры настроены
|
||||
When пользователь переключает тёмную тему (вызывает map.setStyle())
|
||||
Then слой публичных треков восстанавливается
|
||||
And линии видны с теми же цветами по тому же color-mode
|
||||
And фильтры активности/источника сохранены
|
||||
|
||||
Scenario: Переключение спутник→схема
|
||||
Given слой включён, активен спутник
|
||||
When пользователь переключается на схему
|
||||
Then слой остаётся видим, halo выключается
|
||||
|
||||
Scenario: Включение hillshade
|
||||
Given слой включён
|
||||
When пользователь включает hillshade
|
||||
Then все 3 трека видны поверх hillshade
|
||||
Then публичные треки остаются видны (поверх hillshade)
|
||||
```
|
||||
|
||||
## AC-10: Метрики кэша в `/api/health`
|
||||
## AC-13: Производительность
|
||||
|
||||
```gherkin
|
||||
Feature: Наблюдаемость кэшей
|
||||
Feature: SLA отклика
|
||||
|
||||
Scenario: Размер кэшей в health-эндпоинте
|
||||
When клиент шлёт GET /api/health
|
||||
Then ответ содержит поля gpx_fetch_cache_size и gpx_osm_cache_size
|
||||
And значения — целые числа ≥ 0
|
||||
Scenario: GeoJSON p95
|
||||
When 100 запросов GET /api/gps-tracks?bbox=… с ≤ 500 треков в bbox
|
||||
Then p95 ≤ 300 мс
|
||||
|
||||
Scenario: MVT cold
|
||||
When запрос MVT-тайла без кэша
|
||||
Then p95 ≤ 200 мс
|
||||
|
||||
Scenario: MVT hot
|
||||
When повторный запрос того же тайла
|
||||
Then ≤ 20 мс, X-Cache: HIT
|
||||
|
||||
Scenario: Pan/zoom без фризов
|
||||
Given слой включён с 500 треками в видимой области
|
||||
When пользователь делает 10 быстрых pan-операций
|
||||
Then нет видимых фризов (FPS ≥ 30 на десктопе)
|
||||
```
|
||||
|
||||
## AC-11: Производительность
|
||||
## AC-14: Защита от шторма запросов
|
||||
|
||||
```gherkin
|
||||
Feature: Лимиты времени отклика
|
||||
Feature: Debounce и AbortController
|
||||
|
||||
Scenario: OSM bbox запрос с кэш-хитом
|
||||
Given bbox в кэше
|
||||
Then GET /api/gpx/osm/traces возвращается за ≤ 50 мс (p95)
|
||||
Scenario: Быстрый pan не плодит запросов
|
||||
Given слой включён на z ≥ 12
|
||||
When пользователь делает 5 быстрых pan-операций за 1 секунду
|
||||
Then выполняется не более 2 запросов /api/gps-tracks (debounce 500ms)
|
||||
And предыдущие запросы отменены AbortController
|
||||
|
||||
Scenario: URL-импорт малого файла (1 МБ)
|
||||
Then GET /api/gpx/fetch для 1 МБ файла завершается за ≤ 2 сек
|
||||
|
||||
Scenario: OSM bbox запрос без кэша
|
||||
Then GET /api/gpx/osm/traces без кэша возвращается за ≤ 3 сек (p95)
|
||||
Scenario: На z < 8 запросов нет
|
||||
Given пользователь на z=5
|
||||
When пользователь панит карту
|
||||
Then запросов /api/gps-tracks?bbox=… не выполняется
|
||||
```
|
||||
|
||||
## AC-15: Атрибуция
|
||||
|
||||
```gherkin
|
||||
Feature: Атрибуция источников
|
||||
|
||||
Scenario: На карте видна атрибуция
|
||||
Given слой включён, включены OSM и EnduroRussia
|
||||
Then в правом нижнем углу карты отображается строка
|
||||
«© OpenStreetMap contributors (ODbL) | EnduroRussia.ru»
|
||||
|
||||
Scenario: Popup содержит ссылку на оригинал
|
||||
Given пользователь открыл popup трека
|
||||
Then в нём видна ссылка «↗» на источник (или несколько)
|
||||
When пользователь кликает на ссылку
|
||||
Then открывается новая вкладка с оригиналом
|
||||
```
|
||||
|
||||
## AC-16: Безопасность и юридические гарантии
|
||||
|
||||
```gherkin
|
||||
Feature: Юридический минимум
|
||||
|
||||
Scenario: Источник без ADR не активируется
|
||||
Given оператор пытается включить новый source в gps_sources.yaml
|
||||
But ADR licensing-review отсутствует
|
||||
Then pipeline-tests падают (CI блокирует merge)
|
||||
|
||||
Scenario: Pipeline не сохраняет запрещённые поля
|
||||
Given source-ADR требует не сохранять `user`
|
||||
When pipeline получает трек с user='Vasya'
|
||||
Then в БД user=NULL для этой записи
|
||||
|
||||
Scenario: Удаление по запросу автора
|
||||
Given автор оригинала пометил трек удалённым на источнике
|
||||
When следующий прогон pipeline обнаруживает это
|
||||
Then запись в нашей БД помечается как удалённая или удаляется
|
||||
```
|
||||
|
||||
## AC-17: Расширяемость на новые регионы
|
||||
|
||||
```gherkin
|
||||
Feature: Добавление региона ≤ 30 строк YAML
|
||||
|
||||
Scenario: Добавление «Северный Кавказ»
|
||||
Given существующий config/gps_regions.yaml
|
||||
When разработчик добавляет YAML-блок региона: id, name, bbox, enabled,
|
||||
sources (≤ 30 строк суммарно)
|
||||
And запускает pipeline
|
||||
Then регион обрабатывается всеми указанными источниками
|
||||
And никаких правок Python-файлов не требуется
|
||||
And в /api/gps-tracks/health новый регион виден в last_pipeline_run.regions
|
||||
```
|
||||
|
||||
@@ -2,423 +2,564 @@
|
||||
type: test-plan
|
||||
work_item_id: ET-008
|
||||
title: "Test Plan: GPS-треки с публичных платформ на карте"
|
||||
version: 1
|
||||
version: 2
|
||||
status: draft
|
||||
created_at: 2026-06-01
|
||||
updated_at: 2026-06-01
|
||||
changelog:
|
||||
- "v2 (2026-06-01): полная переработка под BRD/TRZ/AC v2 — серверная агрегация, дедупликация, MVT, фильтры активности/источника. Предыдущая v1 описывала URL-импорт + OSM live-поиск."
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
|
||||
test_suites:
|
||||
|
||||
- name: unit-gpx-proxy-validation
|
||||
- name: unit-config-loader
|
||||
type: unit
|
||||
description: "SSRF-валидация URL в gpx_proxy.is_safe_url()"
|
||||
description: "Загрузка и валидация YAML-конфигов sources/regions"
|
||||
cases:
|
||||
- id: U-01
|
||||
name: "Принимает валидный публичный HTTPS URL"
|
||||
input: "https://example.com/track.gpx (резолвится в публичный IP)"
|
||||
expected: "is_safe_url() возвращает True"
|
||||
name: "Валидный gps_sources.yaml парсится"
|
||||
input: "Корректный YAML с 3 источниками"
|
||||
expected: "Возвращает список объектов Source с обязательными полями"
|
||||
|
||||
- id: U-02
|
||||
name: "Отклоняет схему ftp://"
|
||||
input: "ftp://example.com/track.gpx"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
name: "Источник без license_adr — ошибка"
|
||||
input: "YAML с enabled=true, но без license_adr"
|
||||
expected: "ConfigError: 'enabled source requires license_adr'"
|
||||
|
||||
- id: U-03
|
||||
name: "Отклоняет схему file://"
|
||||
input: "file:///etc/passwd"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
name: "Регион с unknown source — ошибка"
|
||||
input: "regions.sources содержит ID, которого нет в sources.yaml"
|
||||
expected: "ConfigError: 'unknown source id'"
|
||||
|
||||
- id: U-04
|
||||
name: "Отклоняет loopback IP"
|
||||
input: "http://127.0.0.1/x.gpx"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
name: "Bbox региона валидируется"
|
||||
input: "bbox=[200, 100, 250, 150]"
|
||||
expected: "ConfigError: 'bbox out of valid range'"
|
||||
|
||||
- id: U-05
|
||||
name: "Отклоняет приватный IP (10.0.0.0/8)"
|
||||
input: "http://10.1.2.3/x.gpx"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
|
||||
- id: U-06
|
||||
name: "Отклоняет приватный IP (192.168.0.0/16)"
|
||||
input: "http://192.168.1.1/x.gpx"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
|
||||
- id: U-07
|
||||
name: "Отклоняет приватный IP (172.16.0.0/12)"
|
||||
input: "http://172.16.0.1/x.gpx"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
|
||||
- id: U-08
|
||||
name: "Отклоняет link-local IP (169.254.x.x)"
|
||||
input: "http://169.254.169.254/metadata"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
|
||||
- id: U-09
|
||||
name: "Отклоняет невалидный URL"
|
||||
input: "not a url"
|
||||
expected: "is_safe_url() возвращает False (без exception)"
|
||||
name: "Disabled source игнорируется в pipeline"
|
||||
input: "Регион ссылается на disabled source"
|
||||
expected: "Pipeline пропускает этот source, warning в логе"
|
||||
|
||||
- name: unit-dedup
|
||||
type: unit
|
||||
description: "compute_dedup_key и merge-логика"
|
||||
cases:
|
||||
- id: U-10
|
||||
name: "Отклоняет хост, который не резолвится"
|
||||
input: "http://nonexistent-host-xyz-12345.invalid/x.gpx"
|
||||
expected: "is_safe_url() возвращает False"
|
||||
name: "Два трека с одинаковым bbox+length+date → один ключ"
|
||||
input: "geom1, geom2 с близкими bounds, length_m differ < 5%, dates same day"
|
||||
expected: "compute_dedup_key(g1) == compute_dedup_key(g2)"
|
||||
|
||||
- id: U-11
|
||||
name: "Разные даты → разные ключи"
|
||||
input: "Те же bbox+length, daty отличаются на 2 дня"
|
||||
expected: "compute_dedup_key различаются"
|
||||
|
||||
- id: U-12
|
||||
name: "Bbox-округление до 0.01°"
|
||||
input: "geom1.bounds=(37.6173, 55.7558, …), geom2.bounds=(37.6171, 55.7559, …)"
|
||||
expected: "Один ключ (округление до 2 знаков)"
|
||||
|
||||
- id: U-13
|
||||
name: "Merge: union sources"
|
||||
input: "track в БД с sources=['osm'], новый с source='enduro_russia', тот же dedup_key"
|
||||
expected: "Запись в БД обновлена: sources=['osm','enduro_russia']"
|
||||
|
||||
- id: U-14
|
||||
name: "Merge: union external_urls"
|
||||
input: "track в БД с external_urls=[...A], новый с [...B], тот же dedup_key"
|
||||
expected: "В БД external_urls=[...A,...B] без дубликатов"
|
||||
|
||||
- id: U-15
|
||||
name: "Merge: приоритет metadata по порядку sources.yaml"
|
||||
input: "OSM (priority 1) собрал name='X', EnduroRussia (priority 2) собрал name='Y' с тем же dedup_key"
|
||||
expected: "В БД name='X' (приоритет первого source)"
|
||||
|
||||
- name: unit-activity-mapping
|
||||
type: unit
|
||||
description: "Маппинг категорий источников в ACTIVITY_TYPES"
|
||||
cases:
|
||||
- id: U-20
|
||||
name: "OSM tag 'enduro' → 'enduro'"
|
||||
input: "['enduro', 'motorcycle']"
|
||||
expected: "'enduro'"
|
||||
|
||||
- id: U-21
|
||||
name: "OSM tag 'mtb' → 'bicycle'"
|
||||
input: "['mtb']"
|
||||
expected: "'bicycle'"
|
||||
|
||||
- id: U-22
|
||||
name: "Unknown tag → 'other'"
|
||||
input: "['xyz']"
|
||||
expected: "'other'"
|
||||
|
||||
- id: U-23
|
||||
name: "Пустой список тэгов → 'other'"
|
||||
input: "[]"
|
||||
expected: "'other'"
|
||||
|
||||
- name: unit-bbox-validation
|
||||
type: unit
|
||||
description: "Валидация bbox в osm_traces"
|
||||
cases:
|
||||
- id: U-20
|
||||
name: "Принимает малый bbox"
|
||||
input: "bbox=[37.6, 55.7, 37.7, 55.8] (0.01 deg²)"
|
||||
expected: "validate_bbox() возвращает True"
|
||||
|
||||
- id: U-21
|
||||
name: "Отклоняет bbox > 0.25 deg²"
|
||||
input: "bbox=[37.0, 55.0, 38.0, 56.0] (1.0 deg²)"
|
||||
expected: "validate_bbox() возвращает False"
|
||||
|
||||
- id: U-22
|
||||
name: "Отклоняет невалидные координаты"
|
||||
input: "bbox=[200, 100, 250, 150]"
|
||||
expected: "validate_bbox() возвращает False"
|
||||
|
||||
- id: U-23
|
||||
name: "Отклоняет перевёрнутый bbox (west > east)"
|
||||
input: "bbox=[38.0, 55.0, 37.0, 56.0]"
|
||||
expected: "validate_bbox() возвращает False"
|
||||
|
||||
- name: unit-cache
|
||||
type: unit
|
||||
description: "LRU кэш с TTL"
|
||||
description: "Валидация bbox в /api/gps-tracks"
|
||||
cases:
|
||||
- id: U-30
|
||||
name: "TTL истёк → cache miss"
|
||||
input: "Положить запись с TTL 1 сек, ждать 2 сек, запросить"
|
||||
expected: "Возвращает None (или вызывает loader)"
|
||||
name: "Валидный bbox"
|
||||
input: "bbox=37.0,55.0,38.0,56.0"
|
||||
expected: "validate_bbox() = True"
|
||||
|
||||
- id: U-31
|
||||
name: "LRU вытеснение при переполнении"
|
||||
input: "Заполнить кэш max=4 записями, добавить 5-ю"
|
||||
expected: "Первая (LRU) запись вытеснена"
|
||||
name: "bbox out-of-range"
|
||||
input: "bbox=200,100,250,150"
|
||||
expected: "validate_bbox() = False"
|
||||
|
||||
- id: U-32
|
||||
name: "Округление bbox-ключа до 4 знаков"
|
||||
input: "bbox=[37.6172999, 55.7558001, ...] и bbox=[37.6173, 55.7558, ...]"
|
||||
expected: "Один и тот же кэш-ключ → cache hit"
|
||||
name: "Перевёрнутый bbox"
|
||||
input: "bbox=38,55,37,56 (west > east)"
|
||||
expected: "validate_bbox() = False"
|
||||
|
||||
- id: U-33
|
||||
name: "URL > 5 МБ не кэшируется"
|
||||
input: "Положить запись размером 6 МБ"
|
||||
expected: "Запись не попадает в кэш (cache.get → None)"
|
||||
name: "Невалидный формат"
|
||||
input: "bbox=foo"
|
||||
expected: "validate_bbox() = False"
|
||||
|
||||
- name: unit-osm-parser
|
||||
type: unit
|
||||
description: "Парсинг OSM trackpoints GPX → JSON"
|
||||
description: "Парсер OSM trackpoints"
|
||||
cases:
|
||||
- id: U-40
|
||||
name: "Извлечение точек из GPX 1.0"
|
||||
input: "GPX с 1 <trk>, 1 <trkseg>, 50 <trkpt>"
|
||||
expected: "JSON: {tracks: [{points_count: 50, distance_km: ~X}]}"
|
||||
name: "Группировка trkpt по gpx_id"
|
||||
input: "GPX 1.0 с trkpt разных gpx_id"
|
||||
expected: "Возвращает по треку на каждый gpx_id"
|
||||
|
||||
- id: U-41
|
||||
name: "Расчёт длины через Haversine"
|
||||
input: "GPX с 3 точками: [37.6,55.7], [37.7,55.8], [37.8,55.9]"
|
||||
expected: "distance_km ≈ 28.3 (±0.5)"
|
||||
name: "Анонимные точки (без gpx_id) — пропуск"
|
||||
input: "GPX с точками без gpx_id"
|
||||
expected: "Эти точки не попадают в результат"
|
||||
|
||||
- id: U-42
|
||||
name: "Пустой GPX (нет trkpt)"
|
||||
input: "GPX без точек"
|
||||
expected: "JSON: {tracks: [], total_points: 0}"
|
||||
name: "Bbox-разбиение региона"
|
||||
input: "region.bbox=(37, 55, 39, 57), cell_size=0.25"
|
||||
expected: "len(cells) = 8 * 8 = 64"
|
||||
|
||||
- id: U-43
|
||||
name: "Защита от XXE (defusedxml)"
|
||||
input: "GPX с DOCTYPE и внешней entity"
|
||||
expected: "Парсер не выполняет загрузку внешней entity (или бросает ошибку)"
|
||||
name: "Расчёт length_m через Haversine"
|
||||
input: "trkpt: [37.6,55.7], [37.7,55.8], [37.8,55.9]"
|
||||
expected: "length_m ≈ 28300 (±500)"
|
||||
|
||||
- name: unit-web-gpx-source
|
||||
- id: U-44
|
||||
name: "Защита от XXE"
|
||||
input: "GPX с DOCTYPE и внешней entity"
|
||||
expected: "defusedxml блокирует, парсер не выполняет загрузку"
|
||||
|
||||
- id: U-45
|
||||
name: "Тэги из GPX → activity_type"
|
||||
input: "<tag>enduro</tag><tag>motorcycle</tag>"
|
||||
expected: "activity_type='enduro'"
|
||||
|
||||
- name: unit-mvt-generation
|
||||
type: unit
|
||||
description: "Расширение модели window.gpxTracks полем source"
|
||||
description: "Генерация MVT-тайлов для gps_tracks"
|
||||
cases:
|
||||
- id: U-50
|
||||
name: "Импорт по URL: source.kind='url'"
|
||||
input: "importGpxFromUrl('https://github.com/x/y.gpx', mockedFetch)"
|
||||
expected: "Трек добавлен с source={kind:'url', url:'https://github.com/x/y.gpx'}"
|
||||
name: "Тайл z=10 с 50 треками"
|
||||
input: "tile_to_bbox(10, x, y), 50 треков в bbox"
|
||||
expected: "Валидный MVT с layer gps_tracks, 50 features"
|
||||
|
||||
- id: U-51
|
||||
name: "Импорт OSM: source.kind='osm'"
|
||||
input: "importOsmTrace({osm_page:0, osm_bbox:[...], gpx_url:'...'}, mockedFetch)"
|
||||
expected: "Трек добавлен с source={kind:'osm', osm_page:0, osm_bbox:[...], url:'...'}"
|
||||
name: "Упрощение геометрии на z=7"
|
||||
input: "Трек 1000 точек, z=7"
|
||||
expected: "После simplify_coords ≤ 100 точек"
|
||||
|
||||
- id: U-52
|
||||
name: "Обратная совместимость: трек без source читается как 'file'"
|
||||
input: "window.gpxTracks[0] без поля source"
|
||||
expected: "renderSourceRow() возвращает '📁 локальный файл'"
|
||||
name: "Min-length фильтр на z ≤ 7"
|
||||
input: "Треки с length_m=500 и 5000 на z=7"
|
||||
expected: "Только трек ≥ 2000м попадает в тайл (min_length для z≤7)"
|
||||
|
||||
- id: U-53
|
||||
name: "Hostname extraction для URL-источника"
|
||||
input: "source.url='https://raw.githubusercontent.com/user/repo/main/track.gpx'"
|
||||
expected: "renderSourceRow() возвращает '🔗 raw.githubusercontent.com'"
|
||||
name: "Properties в feature"
|
||||
input: "Track в БД"
|
||||
expected: "feature.properties содержит id, activity, source, sources,
|
||||
length_km, name, ext_url"
|
||||
|
||||
- name: integration-gpx-fetch
|
||||
- id: U-54
|
||||
name: "Пустой тайл"
|
||||
input: "Bbox без треков"
|
||||
expected: "build_mvt() возвращает b'' (или валидный пустой MVT)"
|
||||
|
||||
- name: unit-color-palette
|
||||
type: unit
|
||||
description: "Цветовая палитра по источнику и активности"
|
||||
cases:
|
||||
- id: U-60
|
||||
name: "Color by source: OSM = #3cb44b"
|
||||
input: "feature.source='osm'"
|
||||
expected: "Match-expression возвращает '#3cb44b'"
|
||||
|
||||
- id: U-61
|
||||
name: "Color by activity: enduro = #e6194b"
|
||||
input: "feature.activity='enduro'"
|
||||
expected: "'#e6194b'"
|
||||
|
||||
- id: U-62
|
||||
name: "Unknown source → fallback"
|
||||
input: "feature.source='unknown'"
|
||||
expected: "'#808080' (или fallback из палитры)"
|
||||
|
||||
- name: integration-pipeline
|
||||
type: integration
|
||||
description: "GET /api/gpx/fetch — прокси с реальным HTTP"
|
||||
description: "Pipeline gps_collect.py end-to-end с mock-источниками"
|
||||
cases:
|
||||
- id: I-01
|
||||
name: "Успешная загрузка GPX по URL (mock-сервер)"
|
||||
input: "GET /api/gpx/fetch?url=http://test-server/track.gpx"
|
||||
expected: "200, Content-Type: application/gpx+xml, тело = GPX, X-Cache: MISS"
|
||||
name: "Полный прогон с 1 mock-источником"
|
||||
input: "Mock OSM API → 100 треков; пустая БД"
|
||||
expected: "После прогона в БД 100 tracks, pipeline_runs.status='ok',
|
||||
tracks_new=100, tracks_updated=0"
|
||||
|
||||
- id: I-02
|
||||
name: "Повторный запрос — cache hit"
|
||||
input: "GET тот же URL"
|
||||
expected: "200, X-Cache: HIT, время ≤ 50 мс"
|
||||
name: "Повторный прогон того же источника — все треки updated"
|
||||
input: "Тот же mock + та же БД с предыдущей записью"
|
||||
expected: "tracks_new=0, tracks_updated=100"
|
||||
|
||||
- id: I-03
|
||||
name: "Отклонение приватного IP"
|
||||
input: "GET /api/gpx/fetch?url=http://127.0.0.1/x.gpx"
|
||||
expected: "400, JSON {error: ...}"
|
||||
name: "Прогон двух источников с пересечением"
|
||||
input: "OSM mock = 100 треков, EnduroRussia mock = 50, из них 20 — те же по dedup_key"
|
||||
expected: "В БД 130 уникальных записей (100 + 50 - 20). 20 пересекающихся имеют sources=['osm','enduro_russia']"
|
||||
|
||||
- id: I-04
|
||||
name: "Отклонение редиректа на приватный IP"
|
||||
input: "Внешний URL → 302 на http://127.0.0.1/x.gpx"
|
||||
expected: "400, JSON {error: ...}"
|
||||
name: "Падение одного источника"
|
||||
input: "OSM mock OK, EnduroRussia mock возвращает 503"
|
||||
expected: "OSM треки в БД, EnduroRussia status='error' в pipeline_runs,
|
||||
но pipeline exit=0 (не strict-mode)"
|
||||
|
||||
- id: I-05
|
||||
name: "Внешний 404"
|
||||
input: "URL ведёт на несуществующий путь"
|
||||
expected: "404, JSON {error: ...}"
|
||||
name: "Dry-run"
|
||||
input: "Любой источник + флаг --dry-run"
|
||||
expected: "БД не меняется, pipeline_runs не пишется,
|
||||
stdout содержит план"
|
||||
|
||||
- id: I-06
|
||||
name: "Лимит размера 50 МБ"
|
||||
input: "Mock-сервер стримит 60 МБ"
|
||||
expected: "413, соединение прервано до конца"
|
||||
name: "Rate-limit соблюдается"
|
||||
input: "Mock source с rate_limit_sec=2, 5 запросов"
|
||||
expected: "Суммарное время ≥ 8 сек (4 интервала × 2 сек)"
|
||||
|
||||
- id: I-07
|
||||
name: "Таймаут"
|
||||
input: "Mock-сервер ничего не отвечает"
|
||||
expected: "504 после 15 сек"
|
||||
name: "Backoff на 429"
|
||||
input: "Mock source первый раз 429, второй раз 200"
|
||||
expected: "Pipeline делает retry после exponential backoff,
|
||||
трек собран"
|
||||
|
||||
- id: I-08
|
||||
name: "URL > 5 МБ не попадает в кэш"
|
||||
input: "Запросить URL с ответом 6 МБ дважды"
|
||||
expected: "Второй запрос: X-Cache: MISS, внешний запрос повторно выполнен"
|
||||
|
||||
- name: integration-osm-traces
|
||||
- name: integration-endpoint-geojson
|
||||
type: integration
|
||||
description: "GET /api/gpx/osm/traces — OSM API клиент"
|
||||
description: "/api/gps-tracks GeoJSON"
|
||||
cases:
|
||||
- id: I-20
|
||||
name: "Bbox-запрос с результатами"
|
||||
input: "GET /api/gpx/osm/traces?bbox=37.6,55.7,37.65,55.75 (mock OSM API)"
|
||||
expected: "200, JSON с tracks[], каждый имеет points_count, distance_km, gpx_url"
|
||||
name: "Малый bbox с фильтрами"
|
||||
input: "GET /api/gps-tracks?bbox=...&activity=enduro&source=osm"
|
||||
expected: "200, FeatureCollection только enduro+OSM треков"
|
||||
|
||||
- id: I-21
|
||||
name: "Bbox > 0.25 deg² → 400"
|
||||
input: "bbox=37,55,38,56"
|
||||
expected: "400, error 'bbox too large'"
|
||||
name: "Truncation"
|
||||
input: "В bbox 1500 треков, limit=500"
|
||||
expected: "returned=500, total_in_bbox=1500, truncated=true"
|
||||
|
||||
- id: I-22
|
||||
name: "OSM API недоступен → 502"
|
||||
input: "OSM mock возвращает 500"
|
||||
expected: "502, JSON error"
|
||||
name: "Невалидный bbox → 400"
|
||||
input: "bbox=foo"
|
||||
expected: "400, JSON error"
|
||||
|
||||
- id: I-23
|
||||
name: "Cache hit на повторный bbox"
|
||||
input: "Тот же bbox дважды"
|
||||
expected: "Второй запрос: внешний запрос НЕ выполнен, ответ из кэша"
|
||||
name: "Bbox в океане → пустой результат"
|
||||
input: "bbox=0,0,1,1"
|
||||
expected: "200, features=[], total=0"
|
||||
|
||||
- id: I-24
|
||||
name: "Пустой bbox → пустой список"
|
||||
input: "bbox в океане"
|
||||
expected: "200, tracks=[], has_more=false"
|
||||
name: "CORS headers"
|
||||
input: "Origin: https://example.com"
|
||||
expected: "Response содержит Access-Control-Allow-Origin: *"
|
||||
|
||||
- id: I-25
|
||||
name: "Пагинация"
|
||||
input: "page=0 возвращает has_more=true, page=1 возвращает следующие"
|
||||
expected: "Корректное смещение, оба запроса валидны"
|
||||
name: "Производительность"
|
||||
input: "100 запросов на bbox с 500 треков"
|
||||
expected: "p95 ≤ 300 мс"
|
||||
|
||||
- name: integration-health-metrics
|
||||
- name: integration-endpoint-mvt
|
||||
type: integration
|
||||
description: "Метрики кэшей в /api/health"
|
||||
description: "/api/gps-tracks/tiles/{z}/{x}/{y}.mvt"
|
||||
cases:
|
||||
- id: I-30
|
||||
name: "Health возвращает размеры кэшей"
|
||||
input: "GET /api/health"
|
||||
expected: "JSON содержит gpx_fetch_cache_size, gpx_osm_cache_size (числа ≥ 0)"
|
||||
name: "Тайл MVT отдаётся"
|
||||
input: "GET /api/gps-tracks/tiles/10/623/325.mvt"
|
||||
expected: "200, Content-Type: application/x-protobuf,
|
||||
X-Cache: MISS"
|
||||
|
||||
- id: I-31
|
||||
name: "Счётчики растут после запросов"
|
||||
input: "После N успешных fetch и M osm_traces запросов"
|
||||
expected: "Размеры кэшей отражают добавленные записи"
|
||||
name: "Cache hit"
|
||||
input: "Повторный запрос того же тайла"
|
||||
expected: "X-Cache: HIT, ≤ 20 мс"
|
||||
|
||||
- name: e2e-url-import
|
||||
- id: I-32
|
||||
name: "Невалидные z/x/y"
|
||||
input: "z=25 / x вне диапазона"
|
||||
expected: "400"
|
||||
|
||||
- id: I-33
|
||||
name: "Очистка кэша"
|
||||
input: "POST /api/gps-tracks/cache/clear, повторный запрос тайла"
|
||||
expected: "X-Cache: MISS"
|
||||
|
||||
- name: integration-endpoint-health
|
||||
type: integration
|
||||
description: "/api/gps-tracks/health"
|
||||
cases:
|
||||
- id: I-40
|
||||
name: "Полный отчёт"
|
||||
input: "GET /api/gps-tracks/health"
|
||||
expected: "200, JSON со всеми полями (см. REQ-F-12)"
|
||||
|
||||
- id: I-41
|
||||
name: "БД отсутствует"
|
||||
input: "Удалить data/gps_tracks.sqlite, GET /api/gps-tracks/health"
|
||||
expected: "503 или 200 с tracks_total=0 и warning"
|
||||
|
||||
- id: I-42
|
||||
name: "Счётчики корректны"
|
||||
input: "БД с 100 OSM + 50 EnduroRussia"
|
||||
expected: "tracks_by_source: {osm: 100, enduro_russia: 50}"
|
||||
|
||||
- name: integration-web-layer
|
||||
type: integration
|
||||
description: "Клиентский слой публичных треков"
|
||||
cases:
|
||||
- id: I-50
|
||||
name: "Включение/выключение слоя"
|
||||
input: "Симуляция click на #public-tracks-cb"
|
||||
expected: "map.getSource('gps-tracks-tiles') существует,
|
||||
layer 'gps-tracks-layer' visibility=visible"
|
||||
|
||||
- id: I-51
|
||||
name: "Фильтр по активности через setFilter"
|
||||
input: "filters.activities = ['enduro']"
|
||||
expected: "map.getFilter('gps-tracks-layer') содержит ['in', ['get','activity'], ['literal',['enduro']]]"
|
||||
|
||||
- id: I-52
|
||||
name: "Переключение color-mode"
|
||||
input: "Переключить с source на activity"
|
||||
expected: "Layer paint['line-color'] переустановлен на activity-палитру"
|
||||
|
||||
- id: I-53
|
||||
name: "GeoJSON-загрузка при z ≥ 12"
|
||||
input: "map.zoom=14, moveend"
|
||||
expected: "Через 500мс debounce — fetch /api/gps-tracks?bbox=…"
|
||||
|
||||
- id: I-54
|
||||
name: "AbortController при быстром pan"
|
||||
input: "Два moveend подряд за 100мс"
|
||||
expected: "Первый fetch отменён, выполняется только второй"
|
||||
|
||||
- id: I-55
|
||||
name: "Halo на спутнике"
|
||||
input: "applyBaseLayer('satellite'), public-tracks включен"
|
||||
expected: "layer 'gps-tracks-halo-satellite' visibility=visible"
|
||||
|
||||
- id: I-56
|
||||
name: "Halo выключен на схеме"
|
||||
input: "applyBaseLayer('schematic')"
|
||||
expected: "halo visibility=none"
|
||||
|
||||
- id: I-57
|
||||
name: "Сохранение слоя при setStyle"
|
||||
input: "Переключение тёмной темы (switchMapStyle)"
|
||||
expected: "rebuildMapOverlays() → restorePublicTracksState() →
|
||||
слой пересоздан, фильтры применены"
|
||||
|
||||
- name: e2e-pipeline
|
||||
type: e2e
|
||||
description: "Импорт GPX по ссылке — полный сценарий"
|
||||
description: "Полный pipeline на тестовых mock-источниках"
|
||||
cases:
|
||||
- id: E-01
|
||||
name: "URL-импорт валидного трека"
|
||||
name: "Сбор → API → визуализация"
|
||||
steps:
|
||||
- "Открыть приложение"
|
||||
- "Нажать кнопку GPX в нижнем тулбаре"
|
||||
- "Переключиться на вкладку «По ссылке»"
|
||||
- "Вставить URL валидного GPX (тестовый mock)"
|
||||
- "Нажать «Загрузить»"
|
||||
- "Убедиться: индикатор показан, через ≤ 5 сек трек на карте"
|
||||
- "Убедиться: в #gpx-list появилась карточка с источником «🔗 host»"
|
||||
- "Кликнуть на трек → отображается статистика и профиль высот"
|
||||
- "Очистить test-БД"
|
||||
- "Запустить pipeline с mock OSM + mock EnduroRussia"
|
||||
- "Проверить: tracks_total > 0 в /api/gps-tracks/health"
|
||||
- "Открыть веб-интерфейс"
|
||||
- "Включить чекбокс «Публичные треки»"
|
||||
- "Убедиться: на карте видны линии треков"
|
||||
- "Кликнуть по треку → popup с метаданными"
|
||||
|
||||
- id: E-02
|
||||
name: "URL-импорт по Enter"
|
||||
name: "Дедупликация — два прогона"
|
||||
steps:
|
||||
- "Активировать «По ссылке»"
|
||||
- "Вставить URL, нажать Enter"
|
||||
- "Убедиться: трек загружен (как при клике)"
|
||||
- "Запустить pipeline (mock-источники отдают 100 треков)"
|
||||
- "Запомнить tracks_total"
|
||||
- "Запустить pipeline повторно (mock отдаёт те же 100)"
|
||||
- "Убедиться: tracks_total не изменился"
|
||||
- "Убедиться: pipeline_runs.tracks_updated=100"
|
||||
|
||||
- id: E-03
|
||||
name: "Невалидный URL → toast"
|
||||
steps:
|
||||
- "Вставить ftp://x.com/y"
|
||||
- "Нажать «Загрузить»"
|
||||
- "Убедиться: toast «Невалидная ссылка»"
|
||||
- "Убедиться: на карте ничего нового"
|
||||
|
||||
- id: E-04
|
||||
name: "Приватный IP блокируется"
|
||||
steps:
|
||||
- "Вставить http://192.168.1.1/x.gpx"
|
||||
- "Нажать «Загрузить»"
|
||||
- "Убедиться: toast «Эта ссылка недоступна»"
|
||||
|
||||
- id: E-05
|
||||
name: "Не GPX по ссылке"
|
||||
steps:
|
||||
- "Вставить URL HTML-страницы"
|
||||
- "Нажать «Загрузить»"
|
||||
- "Убедиться: toast «По этой ссылке не GPX-файл»"
|
||||
|
||||
- name: e2e-osm-search
|
||||
- name: e2e-ui-filters
|
||||
type: e2e
|
||||
description: "Поиск и импорт OSM треков"
|
||||
description: "UI-фильтры по активности и источнику"
|
||||
cases:
|
||||
- id: E-10
|
||||
name: "Поиск треков в области и импорт"
|
||||
name: "Открытие фильтров и переключение"
|
||||
steps:
|
||||
- "Открыть приложение, отзумиться к области Москвы (zoom 12)"
|
||||
- "Открыть #sheet-gpx, активировать «Найти рядом»"
|
||||
- "Нажать «Найти треки в этой области карты»"
|
||||
- "Убедиться: индикатор, потом список карточек"
|
||||
- "Нажать «Показать» у первой карточки"
|
||||
- "Убедиться: трек появился на карте, fit bounds"
|
||||
- "Убедиться: карточка в найденных получила «✓ Загружен»"
|
||||
- "Убедиться: в #gpx-list появилась карточка с «🌍 OSM #...»"
|
||||
- "Включить чекбокс «Публичные треки»"
|
||||
- "Нажать «Фильтры…» → открывается #sheet-gps-filters"
|
||||
- "Снять все галки активности кроме «Эндуро»"
|
||||
- "Убедиться: на карте видны только enduro-треки"
|
||||
- "Снять «OSM» в источниках"
|
||||
- "Убедиться: OSM enduro-треки скрылись"
|
||||
|
||||
- id: E-11
|
||||
name: "Слишком большая область"
|
||||
name: "Переключение color-mode"
|
||||
steps:
|
||||
- "Отзумиться на всю Россию"
|
||||
- "Активировать «Найти рядом»"
|
||||
- "Нажать «Найти»"
|
||||
- "Убедиться: toast «Слишком большая область, увеличьте zoom»"
|
||||
- "Включить слой"
|
||||
- "Открыть фильтры"
|
||||
- "Выбрать «По активности»"
|
||||
- "Убедиться: цвета линий перерисованы (например, enduro = красный)"
|
||||
- "Перезагрузить страницу"
|
||||
- "Убедиться: color-mode='activity' сохранён"
|
||||
|
||||
- id: E-12
|
||||
name: "Пустая область"
|
||||
name: "Persistence фильтров"
|
||||
steps:
|
||||
- "Перейти к области без треков (океан)"
|
||||
- "Активировать «Найти рядом»"
|
||||
- "Нажать «Найти»"
|
||||
- "Убедиться: сообщение «В этой области нет публичных GPS-треков»"
|
||||
- "Настроить фильтры (только moto, только EnduroRussia)"
|
||||
- "Перезагрузить страницу"
|
||||
- "Открыть фильтры"
|
||||
- "Убедиться: чекбоксы соответствуют настройкам"
|
||||
|
||||
- id: E-13
|
||||
name: "Пагинация"
|
||||
steps:
|
||||
- "Найти треки в области с большим количеством"
|
||||
- "Убедиться: кнопка «Показать ещё» внизу"
|
||||
- "Нажать «Показать ещё»"
|
||||
- "Убедиться: список расширился"
|
||||
|
||||
- id: E-14
|
||||
name: "Повторный импорт → toast"
|
||||
steps:
|
||||
- "Импортировать трек по «Показать»"
|
||||
- "Нажать «Показать» у той же карточки ещё раз"
|
||||
- "Убедиться: toast «Уже загружен»"
|
||||
|
||||
- id: E-15
|
||||
name: "Внешняя ссылка на osm.org"
|
||||
steps:
|
||||
- "Найти треки, нажать «↗» у карточки"
|
||||
- "Убедиться: новая вкладка открыта на openstreetmap.org"
|
||||
|
||||
- name: e2e-mixed-sources
|
||||
- name: e2e-popup
|
||||
type: e2e
|
||||
description: "Совместимость трёх источников в одной сессии"
|
||||
description: "Popup трека"
|
||||
cases:
|
||||
- id: E-20
|
||||
name: "3 трека разных источников"
|
||||
name: "Popup полный набор полей"
|
||||
steps:
|
||||
- "Загрузить 1 локальный файл"
|
||||
- "Загрузить 1 по URL"
|
||||
- "Загрузить 1 из OSM"
|
||||
- "Убедиться: 3 карточки в #gpx-list, разные цвета, разные источники"
|
||||
- "Удалить URL-трек"
|
||||
- "Убедиться: 2 трека на карте, корректные источники"
|
||||
- "Включить слой"
|
||||
- "Кликнуть на трек на карте"
|
||||
- "Убедиться: popup содержит name, activity-иконку, км, дату, user, sources"
|
||||
- "Кликнуть по ссылке источника"
|
||||
- "Убедиться: открыта новая вкладка"
|
||||
|
||||
- id: E-21
|
||||
name: "Сохранение при смене темы"
|
||||
name: "Popup для трека без user"
|
||||
steps:
|
||||
- "Загрузить 3 трека разных источников"
|
||||
- "Переключить тёмную тему"
|
||||
- "Убедиться: все 3 трека на карте"
|
||||
- "Убедиться: источники в карточках сохранены"
|
||||
- "Найти трек без user"
|
||||
- "Кликнуть → popup без строки «Автор»"
|
||||
|
||||
- id: E-22
|
||||
name: "Сохранение при включении hillshade"
|
||||
steps:
|
||||
- "Загрузить 3 трека"
|
||||
- "Включить hillshade"
|
||||
- "Убедиться: все 3 трека видны поверх hillshade"
|
||||
|
||||
- name: e2e-cache
|
||||
- name: e2e-compat
|
||||
type: e2e
|
||||
description: "Поведение кэша через API"
|
||||
description: "Совместимость с другими функциями"
|
||||
cases:
|
||||
- id: E-30
|
||||
name: "Кэш URL-fetch снижает время"
|
||||
name: "Слой + спутник + halo"
|
||||
steps:
|
||||
- "GET /api/gpx/fetch?url=<test-url> — измерить t1"
|
||||
- "GET /api/gpx/fetch?url=<тот же url> — измерить t2"
|
||||
- "Убедиться: t2 < 100 мс, заголовок X-Cache: HIT"
|
||||
- "Включить «Публичные треки»"
|
||||
- "Переключить подложку на «Спутник»"
|
||||
- "Убедиться: треки видны на спутнике с белой обводкой"
|
||||
|
||||
- id: E-31
|
||||
name: "Размеры кэша в health"
|
||||
name: "Слой + тёмная тема"
|
||||
steps:
|
||||
- "Сделать N запросов /api/gpx/fetch"
|
||||
- "GET /api/health"
|
||||
- "Убедиться: gpx_fetch_cache_size == N (или min(N, лимит))"
|
||||
- "Включить слой"
|
||||
- "Переключить тёмную тему"
|
||||
- "Убедиться: треки остаются на карте"
|
||||
- "Убедиться: фильтры сохранены"
|
||||
|
||||
- id: E-32
|
||||
name: "Слой + личный GPX (ET-006)"
|
||||
steps:
|
||||
- "Включить слой"
|
||||
- "Загрузить личный GPX"
|
||||
- "Убедиться: оба видны"
|
||||
- "Убедиться: личный трек выше публичных по z-order"
|
||||
|
||||
- id: E-33
|
||||
name: "Слой + маршрут OSRM"
|
||||
steps:
|
||||
- "Включить слой"
|
||||
- "Построить маршрут OSRM"
|
||||
- "Убедиться: маршрут OSRM визуально выше публичных треков"
|
||||
|
||||
- id: E-34
|
||||
name: "Слой + hillshade"
|
||||
steps:
|
||||
- "Включить слой"
|
||||
- "Включить hillshade"
|
||||
- "Убедиться: оба видны"
|
||||
|
||||
- name: e2e-low-zoom-protection
|
||||
type: e2e
|
||||
description: "Защита от шторма запросов на low-zoom"
|
||||
cases:
|
||||
- id: E-40
|
||||
name: "Слой скрыт на z<8"
|
||||
steps:
|
||||
- "Включить слой"
|
||||
- "Отзумиться до z=5"
|
||||
- "Убедиться: линии не отображаются"
|
||||
- "Убедиться: появилась подсказка «Зум 8+» у чекбокса"
|
||||
|
||||
- id: E-41
|
||||
name: "Pan на z 14 не штормит запросы"
|
||||
steps:
|
||||
- "Включить слой, z=14"
|
||||
- "Быстро панить карту (5 раз за 1 сек)"
|
||||
- "Проверить network log: не более 2 запросов /api/gps-tracks"
|
||||
|
||||
- name: load-pipeline
|
||||
type: load
|
||||
description: "Нагрузочные сценарии pipeline и API"
|
||||
cases:
|
||||
- id: L-01
|
||||
name: "Полный прогон pipeline на ЦФО+Чувашию (mock)"
|
||||
input: "Mock OSM с реальным объёмом ≈ 50K треков"
|
||||
expected: "Прогон завершается за ≤ 6 часов (cron-окно)"
|
||||
|
||||
- id: L-02
|
||||
name: "API под нагрузкой"
|
||||
input: "10 параллельных клиентов делают по 100 запросов /api/gps-tracks"
|
||||
expected: "p95 ≤ 500 мс, нет ошибок"
|
||||
|
||||
- id: L-03
|
||||
name: "MVT-тайлы под нагрузкой"
|
||||
input: "100 параллельных запросов разных тайлов"
|
||||
expected: "p95 cold ≤ 300 мс, hit-rate кэша > 80% на повторах"
|
||||
|
||||
test_data:
|
||||
- name: "test-track-public.gpx"
|
||||
description: "Валидный GPX 1.1, 1 МБ, для URL-импорта (mock-сервер)"
|
||||
- name: "test-track-large.gpx"
|
||||
description: "GPX 60 МБ — для проверки лимита размера"
|
||||
- name: "test-osm-trackpoints.gpx"
|
||||
description: "Реальный ответ OSM trackpoints API (зафиксирован для mock)"
|
||||
- name: "test-html-page.html"
|
||||
description: "HTML вместо GPX — для проверки валидации формата"
|
||||
- name: "test-xxe-payload.gpx"
|
||||
description: "GPX с DOCTYPE и внешней entity — для проверки defusedxml"
|
||||
- name: "bbox-moscow-small"
|
||||
description: "[37.6, 55.7, 37.65, 55.75] — реальная область с публичными треками OSM"
|
||||
- name: "bbox-too-large"
|
||||
description: "[37.0, 55.0, 38.0, 56.0] — > 0.25 deg² для проверки 400"
|
||||
fixtures_dir: "tests/fixtures/gps-tracks/"
|
||||
fixtures:
|
||||
- name: "osm-trackpoints-bbox-moscow.gpx"
|
||||
description: "Реальный ответ OSM API на bbox центра Москвы"
|
||||
- name: "osm-trackpoints-multipage.json"
|
||||
description: "Серия ответов OSM с has_more=true на нескольких страницах"
|
||||
- name: "enduro-russia-mock-listing.html"
|
||||
description: "Главная страница региона на EnduroRussia (mock)"
|
||||
- name: "enduro-russia-mock-track.gpx"
|
||||
description: "GPX-файл, отдаваемый EnduroRussia mock"
|
||||
- name: "ttrails-mock-track.gpx"
|
||||
description: "GPX от ttrails mock"
|
||||
- name: "xxe-payload.gpx"
|
||||
description: "GPX с DOCTYPE и внешней entity (для проверки defusedxml)"
|
||||
- name: "dedup-pair-osm-enduro.json"
|
||||
description: "Пара треков (одна и та же поездка из двух источников) для проверки dedup"
|
||||
- name: "gps_tracks_seed.sql"
|
||||
description: "SQL-сид: 1000 синтетических треков для интеграционных тестов"
|
||||
|
||||
test_environment:
|
||||
mock_servers:
|
||||
- "Mock HTTP-сервер для /api/gpx/fetch тестов (отдаёт фиксированные ответы)"
|
||||
- "Mock OSM API для /api/gpx/osm/traces тестов"
|
||||
fixtures_dir: "tests/fixtures/gpx-public/"
|
||||
- "Mock OSM API (отвечает на /api/0.6/trackpoints и /api/0.6/gpx/<id>)"
|
||||
- "Mock EnduroRussia.ru (HTML-страницы + GPX-файлы)"
|
||||
- "Mock ttrails.ru"
|
||||
cron_simulation:
|
||||
- "В тестах cron заменяется на pytest fixture, вызывающий run() напрямую"
|
||||
db_isolation:
|
||||
- "Каждый тест использует in-memory или временный sqlite-файл в pytest tmp_path"
|
||||
network:
|
||||
- "Все исходящие HTTP в unit/integration — через httpx_mock или respx (без реальной сети)"
|
||||
notes:
|
||||
- "OSM API в e2e тестах должен мокироваться, чтобы не зависеть от внешней доступности"
|
||||
- "Для нагрузочных тестов кэша использовать pytest-benchmark"
|
||||
- "L-01 (полный прогон pipeline) запускается отдельно, не в обычном CI"
|
||||
- "E2E UI-тесты — Playwright; URL test-среды https://openclaw.mva154.duckdns.org/enduro/ (см. 04b-ui-test-cases.md)"
|
||||
- "Для load-тестов использовать pytest-benchmark + locust"
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
type: ui-test-cases
|
||||
work_item_id: ET-008
|
||||
title: "UI Test Cases: GPS-треки с публичных платформ"
|
||||
version: 1
|
||||
version: 2
|
||||
status: draft
|
||||
created_at: 2026-06-01
|
||||
updated_at: 2026-06-01
|
||||
changelog:
|
||||
- "v2 (2026-06-01): полная переработка под BRD/TRZ v2 — чекбокс «Публичные треки» в попапе, sheet фильтров, halo на спутнике, popup трека. Предыдущая v1 описывала вкладки источников в #sheet-gpx (URL/OSM)."
|
||||
authors:
|
||||
- "agent:analyst"
|
||||
---
|
||||
@@ -14,299 +16,33 @@ authors:
|
||||
|
||||
Базовый URL: `https://openclaw.mva154.duckdns.org/enduro/`
|
||||
|
||||
Все тесты проверяют появление и поведение секции «Источники» в
|
||||
`#sheet-gpx`, импорта по URL и поиска OSM-треков. Внешние сетевые
|
||||
запросы в test-окружении мокаются (см. test-plan).
|
||||
Все тесты проверяют появление и поведение нового слоя «Публичные
|
||||
треки»: чекбокса в `#terrain-popup`, sheet фильтров, отрисовки линий,
|
||||
popup и совместимости со спутниковой подложкой / тёмной темой.
|
||||
|
||||
Селекторы (новые, добавляются ET-008):
|
||||
- `#source-seg` — segmented control «Источники»
|
||||
- `#source-btn-file`, `#source-btn-url`, `#source-btn-nearby` — кнопки вкладок
|
||||
- `#gpx-source-pane-file`, `#gpx-source-pane-url`, `#gpx-source-pane-nearby` — контент-блоки
|
||||
- `#gpx-url-input` — поле ввода URL
|
||||
- `#btn-gpx-fetch-url` — кнопка «Загрузить» URL
|
||||
- `#btn-gpx-find-nearby` — кнопка «Найти треки в этой области»
|
||||
- `#gpx-nearby-results` — контейнер списка найденных
|
||||
- `.gpx-nearby-card` — карточка найденного OSM-трека
|
||||
- `.gnc-import` — кнопка «Показать»
|
||||
- `.gnc-external` — ссылка «↗»
|
||||
- `.gpx-source-row` — индикатор источника в карточке трека
|
||||
|
||||
Существующие селекторы (ET-006): `#tb-gpx`, `#sheet-gpx`, `#gpx-list`.
|
||||
- `#public-tracks-cb` — чекбокс «Публичные треки» в `#terrain-popup`
|
||||
- `#public-tracks-zoom-hint` — подсказка «Зум 8+»
|
||||
- `#public-tracks-filters-btn` — ссылка «Фильтры…»
|
||||
- `#sheet-gps-filters` — bottom sheet фильтров
|
||||
- `#gps-activity-grid` — секция чекбоксов активности
|
||||
- `#gps-source-grid` — секция чекбоксов источников
|
||||
- `#gps-color-by-source`, `#gps-color-by-activity` — переключатель color-mode
|
||||
- `#gps-stat-total`, `#gps-stat-shown` — счётчики в sheet
|
||||
- `.gps-track-popup` — MapLibre Popup с метаданными трека (имя класса
|
||||
можно задать через `setHTML` и контейнер)
|
||||
|
||||
Существующие селекторы: `#terrain-toggle`, `#terrain-popup`,
|
||||
`#btn-theme`, `#base-btn-satellite`, `#base-btn-schematic`,
|
||||
`#terrain-hillshade-cb`, `#tb-gpx`, `#map`.
|
||||
|
||||
Предусловие: тестовая среда содержит pre-collected dataset публичных
|
||||
треков (или mock-backend подменяет `/api/gps-tracks*` фикстурами).
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-01 — Секция «Источники» видна в #sheet-gpx
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. screenshot: "01-sheet-gpx-sources-section"
|
||||
6. check-visual: "В верхней части #sheet-gpx (под заголовком, над списком треков) видна секция «ИСТОЧНИКИ» с тремя кнопками segmented control: «Из файла», «По ссылке», «Найти рядом». По умолчанию активна (подсвечена оранжевым) кнопка «Из файла»."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-02 — Переключение на вкладку «По ссылке»
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-url"
|
||||
6. wait: 500
|
||||
7. screenshot: "02-source-url-pane"
|
||||
8. check-visual: "Кнопка «По ссылке» подсвечена оранжевым, «Из файла» и «Найти рядом» — нет. Под кнопками видно поле ввода с placeholder «https://example.com/track.gpx» и кнопка «Загрузить» справа."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-03 — Переключение на вкладку «Найти рядом»
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-nearby"
|
||||
6. wait: 500
|
||||
7. screenshot: "03-source-nearby-pane"
|
||||
8. check-visual: "Кнопка «Найти рядом» подсвечена оранжевым. Под кнопками видна крупная кнопка «Найти треки в этой области карты». Список найденных треков пуст или отсутствует."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-04 — Поле URL принимает ввод
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-url"
|
||||
6. wait: 500
|
||||
7. click: "#gpx-url-input"
|
||||
8. wait: 200
|
||||
9. screenshot: "04-url-input-focused"
|
||||
10. check-visual: "Поле #gpx-url-input получило фокус (видна рамка/каретка), placeholder виден если поле пустое. Кнопка «Загрузить» рядом, активна (не дизейблена)."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-05 — Невалидный URL: toast об ошибке
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-url"
|
||||
6. wait: 500
|
||||
7. click: "#gpx-url-input"
|
||||
8. wait: 200
|
||||
9. type: "not-a-url"
|
||||
10. click: "#btn-gpx-fetch-url"
|
||||
11. wait: 1000
|
||||
12. screenshot: "05-invalid-url-toast"
|
||||
13. check-visual: "Сверху по центру экрана отображается toast-уведомление с текстом «Невалидная ссылка» (или похожим). Никаких изменений на карте."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-06 — Кнопка «Найти треки» дизейблится во время запроса
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-nearby"
|
||||
6. wait: 500
|
||||
7. click: "#btn-gpx-find-nearby"
|
||||
8. wait: 300
|
||||
9. screenshot: "06-finding-tracks-loading"
|
||||
10. check-visual: "Кнопка «Найти треки в этой области карты» визуально дизейблена (серая / opacity снижен). Виден индикатор загрузки (spinner или moto-wheel)."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-07 — Список найденных треков
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-nearby"
|
||||
6. wait: 500
|
||||
7. click: "#btn-gpx-find-nearby"
|
||||
8. wait: 4000
|
||||
9. screenshot: "07-nearby-tracks-list"
|
||||
10. check-visual: "Под кнопкой поиска появился список карточек .gpx-nearby-card. Каждая карточка содержит: иконку 🌍 (или OSM-логотип) слева, имя/описание трека и метаданные (км, аноним/автор), кнопку «Показать» справа, маленькую ссылку «↗». Карточки разделены тонкими линиями."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-08 — Импорт OSM-трека: трек на карте, индикатор в карточке
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-nearby"
|
||||
6. wait: 500
|
||||
7. click: "#btn-gpx-find-nearby"
|
||||
8. wait: 4000
|
||||
9. click: ".gnc-import"
|
||||
10. wait: 4000
|
||||
11. screenshot: "08-osm-track-imported"
|
||||
12. check-visual: "На карте видна цветная линия импортированного трека. В списке найденных карточка первого трека показывает индикатор «✓ Загружен» вместо кнопки «Показать». В нижней части #sheet-gpx (в #gpx-list) появилась новая карточка трека с источником «🌍 OSM #...»."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-09 — Источник «OSM» — кликабельная ссылка в #gpx-list
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-nearby"
|
||||
6. wait: 500
|
||||
7. click: "#btn-gpx-find-nearby"
|
||||
8. wait: 4000
|
||||
9. click: ".gnc-import"
|
||||
10. wait: 4000
|
||||
11. screenshot: "09-gpx-list-source-osm"
|
||||
12. check-visual: "В нижнем списке #gpx-list карточка импортированного трека под именем содержит строку «.gpx-source-row» с текстом «🌍 OSM #<число>». Текст оформлен как ссылка (подчёркнут или другой цвет)."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-10 — Смешанные источники в #gpx-list
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-url"
|
||||
6. wait: 500
|
||||
7. click: "#gpx-url-input"
|
||||
8. wait: 200
|
||||
9. type: "https://example.test/mock-track.gpx"
|
||||
10. click: "#btn-gpx-fetch-url"
|
||||
11. wait: 4000
|
||||
12. click: "#source-btn-nearby"
|
||||
13. wait: 500
|
||||
14. click: "#btn-gpx-find-nearby"
|
||||
15. wait: 4000
|
||||
16. click: ".gnc-import"
|
||||
17. wait: 4000
|
||||
18. screenshot: "10-mixed-sources-list"
|
||||
19. check-visual: "В #gpx-list 2 карточки: одна с источником «🔗 example.test», вторая с «🌍 OSM #...». Карточки имеют разные цветовые индикаторы слева. Обе видны на карте как линии разных цветов."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-11 — Импорт по URL: трек появляется на карте
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-url"
|
||||
6. wait: 500
|
||||
7. click: "#gpx-url-input"
|
||||
8. wait: 200
|
||||
9. type: "https://example.test/mock-track.gpx"
|
||||
10. click: "#btn-gpx-fetch-url"
|
||||
11. wait: 5000
|
||||
12. screenshot: "11-url-track-loaded"
|
||||
13. check-visual: "На карте видна цветная линия загруженного трека. В #gpx-list появилась карточка с именем «mock-track» и источником «🔗 example.test». Карта выполнила fit bounds — трек по центру экрана."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-12 — Секция «Источники» на мобильном
|
||||
|
||||
- тип: ui
|
||||
- viewport: mobile
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. screenshot: "12-sources-mobile-default"
|
||||
6. check-visual: "На мобильном viewport секция «Источники» помещается по ширине экрана. Три кнопки segmented control видны и нажимаемы, не выходят за экран. Активна «Из файла»."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-13 — Поле URL на мобильном
|
||||
|
||||
- тип: ui
|
||||
- viewport: mobile
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-url"
|
||||
6. wait: 500
|
||||
7. screenshot: "13-url-pane-mobile"
|
||||
8. check-visual: "На мобильном поле #gpx-url-input занимает большую часть ширины, кнопка «Загрузить» справа. Оба элемента не перекрываются, нажимаемы, помещаются в экран."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-14 — Список найденных OSM треков на мобильном
|
||||
|
||||
- тип: ui
|
||||
- viewport: mobile
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-nearby"
|
||||
6. wait: 500
|
||||
7. click: "#btn-gpx-find-nearby"
|
||||
8. wait: 4000
|
||||
9. screenshot: "14-nearby-list-mobile"
|
||||
10. check-visual: "На мобильном карточки .gpx-nearby-card отображаются вертикально, занимают всю ширину. Кнопка «Показать» и ссылка «↗» в каждой карточке нажимаемы, не перекрываются. Список скроллится."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-15 — Совместимость со спутниковой подложкой
|
||||
### TC-UI-01 — Чекбокс «Публичные треки» виден в попапе
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
@@ -316,22 +52,381 @@ authors:
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#base-btn-satellite"
|
||||
6. wait: 5000
|
||||
5. screenshot: "01-popup-with-public-tracks-checkbox"
|
||||
6. check-visual: "В открытом попапе #terrain-popup между секциями «Тропы» и «POI» (после соответствующего разделителя `<hr>`) видна строка «Публичные треки» с чекбоксом #public-tracks-cb. По умолчанию чекбокс снят, ссылка «Фильтры…» не видна."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-02 — Включение слоя «Публичные треки»
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. screenshot: "02-public-tracks-enabled"
|
||||
8. check-visual: "Чекбокс установлен. На карте поверх существующих trail-линий и POI видны цветные линии публичных треков (отдельные линии, не heatmap). Рядом с чекбоксом появилась ссылка «Фильтры…»."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-03 — Подсказка «Зум 8+» на низком зуме
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/?z=5
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 1500
|
||||
7. screenshot: "03-public-tracks-zoom-hint"
|
||||
8. check-visual: "Чекбокс включён, но на карте линии публичных треков не видны. Рядом с чекбоксом (или под ним) отображается подсказка «Зум 8+» (стилем как существующая подсказка «Зум 10+» у hillshade)."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-04 — Открытие sheet фильтров
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 2000
|
||||
7. click: "#public-tracks-filters-btn"
|
||||
8. wait: 800
|
||||
9. screenshot: "04-gps-filters-sheet-open"
|
||||
10. check-visual: "Открылся bottom sheet #sheet-gps-filters с заголовком «Фильтры публичных треков». Видны секции: «ТИП АКТИВНОСТИ» (7 чекбоксов: эндуро, мото, off-road, велосипед, пешком, лыжи, другое), «ИСТОЧНИК» (≥ 3 чекбокса), «ЦВЕТ ЛИНИЙ» (segmented control «По источнику» / «По активности»). По умолчанию все чекбоксы установлены, color-mode='По источнику' активен."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-05 — Фильтрация по активности (клиентская, мгновенная)
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#public-tracks-filters-btn"
|
||||
8. wait: 800
|
||||
9. screenshot: "05a-filters-all-on"
|
||||
10. check-visual: "В sheet видны все 7 чекбоксов активности — установлены. На карте видно много линий разных типов."
|
||||
11. click: "#gps-activity-grid input[value='bicycle']"
|
||||
12. wait: 300
|
||||
13. click: "#gps-activity-grid input[value='hike']"
|
||||
14. wait: 300
|
||||
15. click: "#gps-activity-grid input[value='ski']"
|
||||
16. wait: 300
|
||||
17. click: "#gps-activity-grid input[value='other']"
|
||||
18. wait: 500
|
||||
19. screenshot: "05b-filters-only-moto-types"
|
||||
20. check-visual: "Выключены чекбоксы «Велосипед», «Пешком», «Лыжи», «Другое». На карте линий стало заметно меньше (только enduro/moto/offroad). Счётчик «Видны (фильтр)» в нижней части sheet уменьшился."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-06 — Фильтрация по источнику
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#public-tracks-filters-btn"
|
||||
8. wait: 800
|
||||
9. click: "#gps-source-grid input[value='osm']"
|
||||
10. wait: 500
|
||||
11. screenshot: "06-source-osm-disabled"
|
||||
12. check-visual: "Чекбокс «OSM» снят. На карте все линии цвета OSM (зелёного — при color-by-source) скрыты. Счётчик «Видны» уменьшился."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-07 — Переключение color-mode
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#public-tracks-filters-btn"
|
||||
8. wait: 800
|
||||
9. screenshot: "07a-color-by-source"
|
||||
10. check-visual: "Активна кнопка «По источнику». Линии на карте окрашены по источникам (например, зелёный = OSM, красный = EnduroRussia)."
|
||||
11. click: "#gps-color-by-activity"
|
||||
12. wait: 600
|
||||
13. screenshot: "07b-color-by-activity"
|
||||
14. check-visual: "Активна кнопка «По активности». Линии перекрашены: например, красные = enduro, оранжевые = moto. Кнопка «По источнику» больше не подсвечена."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-08 — Popup при клике на трек
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#map"
|
||||
8. wait: 1500
|
||||
9. screenshot: "08-track-popup"
|
||||
10. check-visual: "При клике на линию трека (предполагается, что под центром карты есть трек) открылся MapLibre Popup. В нём видны: иконка активности (🏍 / 🚴 / …) + текстовая метка, длина в км, дата (если есть), автор (если есть), список источников со ссылками '↗'. Popup имеет крестик закрытия."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-09 — Halo на спутниковой подложке
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#base-btn-satellite"
|
||||
8. wait: 5000
|
||||
9. screenshot: "09-public-tracks-on-satellite"
|
||||
10. check-visual: "Карта показывает спутниковые снимки. Линии публичных треков видны поверх спутника, у каждой линии есть белая (или светлая) обводка-halo для контраста на тёмном фоне. Цвета линий по-прежнему отличаются по источнику/активности."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-10 — Возврат на схему — halo пропадает
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#base-btn-satellite"
|
||||
8. wait: 5000
|
||||
9. click: "#base-btn-schematic"
|
||||
10. wait: 3000
|
||||
11. screenshot: "10-back-to-schematic-no-halo"
|
||||
12. check-visual: "Карта вернулась на схему OSM. Линии публичных треков видны без halo (обычная толщина и цвет). На фоне светлой схемы — без обводки."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-11 — Сохранение слоя при переключении тёмной темы
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#btn-theme"
|
||||
8. wait: 3000
|
||||
9. screenshot: "11-public-tracks-after-theme-switch"
|
||||
10. check-visual: "После переключения темы (например, на тёмную) линии публичных треков остались на карте. Цвета сохранены. На тёмной теме линии хорошо различимы."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-12 — Сохранение слоя при включении hillshade
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#terrain-hillshade-cb"
|
||||
8. wait: 3000
|
||||
9. screenshot: "12-public-tracks-over-hillshade"
|
||||
10. check-visual: "Включён hillshade (тени рельефа). Линии публичных треков остаются видны поверх теней рельефа. Контраст сохраняется."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-13 — Совместимость с маршрутом OSRM (z-order)
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#tb-route"
|
||||
8. wait: 1000
|
||||
9. click: "#map"
|
||||
10. wait: 1500
|
||||
11. scroll: 100
|
||||
12. click: "#map"
|
||||
13. wait: 5000
|
||||
14. screenshot: "13-public-tracks-and-osrm-route"
|
||||
15. check-visual: "Видны и линии публичных треков, и линия маршрута OSRM (синяя/оранжевая). Маршрут OSRM визуально лежит поверх публичных треков (выше по z-order). Обе системы линий читаемы."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-14 — Sheet фильтров на мобильном
|
||||
|
||||
- тип: ui
|
||||
- viewport: mobile
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#public-tracks-filters-btn"
|
||||
8. wait: 800
|
||||
9. screenshot: "14-gps-filters-mobile"
|
||||
10. check-visual: "На мобильном viewport sheet #sheet-gps-filters занимает всю ширину. Все 7 чекбоксов активности видны (например, 2-3 колонки grid). Чекбоксы источников видны. Segmented control color-mode помещается. Все элементы нажимаемы, не перекрываются."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-15 — Включение слоя на мобильном
|
||||
|
||||
- тип: ui
|
||||
- viewport: mobile
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. screenshot: "15-public-tracks-mobile"
|
||||
8. check-visual: "На мобильном устройстве после включения чекбокса линии публичных треков видны на карте. Попап слоёв и тулбар не перекрывают карту целиком — слой просматривается."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-16 — Persistence: слой включён после перезагрузки
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
8. wait: 6000
|
||||
9. screenshot: "16-public-tracks-after-reload"
|
||||
10. check-visual: "После перезагрузки страницы карта сразу показывает линии публичных треков (слой автоматически восстановлен из localStorage). Открытие попапа слоёв должно показать чекбокс установленным."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-17 — Persistence: фильтры сохраняются после перезагрузки
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#public-tracks-filters-btn"
|
||||
8. wait: 800
|
||||
9. click: "#gps-activity-grid input[value='bicycle']"
|
||||
10. wait: 300
|
||||
11. click: "#gps-activity-grid input[value='hike']"
|
||||
12. wait: 300
|
||||
13. click: "#gps-color-by-activity"
|
||||
14. wait: 500
|
||||
15. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
16. wait: 6000
|
||||
17. click: "#terrain-toggle"
|
||||
18. wait: 500
|
||||
19. click: "#public-tracks-filters-btn"
|
||||
20. wait: 800
|
||||
21. screenshot: "17-filters-after-reload"
|
||||
22. check-visual: "Чекбоксы «Велосипед» и «Пешком» по-прежнему сняты. Color-mode = «По активности» (соответствующая кнопка подсвечена). Линии на карте окрашены по активности."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-18 — Атрибуция источников
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. screenshot: "18-attribution-public-tracks"
|
||||
8. check-visual: "В правом нижнем углу карты (в стандартной MapLibre-панели атрибуции) видны строки с атрибуцией источников публичных треков: например, «© OpenStreetMap contributors (ODbL)» и «EnduroRussia.ru» (либо иконка info, при клике на которую разворачивается полный текст)."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-19 — Совместимость с личным GPX (ET-006)
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#tb-gpx"
|
||||
8. wait: 1000
|
||||
9. click: "#source-btn-nearby"
|
||||
10. wait: 500
|
||||
11. click: "#btn-gpx-find-nearby"
|
||||
12. wait: 4000
|
||||
13. click: ".gnc-import"
|
||||
14. wait: 4000
|
||||
15. screenshot: "15-osm-track-on-satellite"
|
||||
16. check-visual: "На спутниковой подложке видна цветная линия импортированного OSM-трека. Линия имеет hover-видимость (контрастная для спутника). Панель #sheet-gpx не конфликтует со спутником визуально."
|
||||
9. screenshot: "19-public-tracks-with-gpx-sheet"
|
||||
10. check-visual: "Открыт sheet #sheet-gpx (для личных треков из ET-006). Слой публичных треков на карте остаётся видимым. Sheet и слой не конфликтуют визуально. Список личных треков в sheet — пустой (если ничего не загружено)."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-16 — Сохранение треков при переключении тёмной темы
|
||||
### TC-UI-20 — Выключение слоя — линии исчезают
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
@@ -339,57 +434,11 @@ authors:
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-nearby"
|
||||
6. wait: 500
|
||||
7. click: "#btn-gpx-find-nearby"
|
||||
8. wait: 4000
|
||||
9. click: ".gnc-import"
|
||||
10. wait: 4000
|
||||
11. click: "#btn-theme"
|
||||
12. wait: 3000
|
||||
13. screenshot: "16-osm-track-after-theme-switch"
|
||||
14. check-visual: "После переключения тёмной темы цветная линия импортированного OSM-трека остаётся на карте. В #gpx-list карточка трека с источником «🌍 OSM #...» сохранилась."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-17 — Сохранение треков при переключении источника «Из файла»
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-nearby"
|
||||
6. wait: 500
|
||||
7. click: "#btn-gpx-find-nearby"
|
||||
8. wait: 4000
|
||||
9. click: ".gnc-import"
|
||||
10. wait: 4000
|
||||
11. click: "#source-btn-file"
|
||||
12. wait: 500
|
||||
13. screenshot: "17-back-to-file-tab"
|
||||
14. check-visual: "Активна вкладка «Из файла», секция «Найти рядом» свернута. Импортированный OSM-трек остаётся в нижнем списке #gpx-list (карточка с «🌍 OSM #...»). Сам трек по-прежнему видим на карте."
|
||||
|
||||
---
|
||||
|
||||
### TC-UI-18 — Внешняя ссылка ↗ на osm.org
|
||||
|
||||
- тип: ui
|
||||
- viewport: desktop
|
||||
|
||||
шаги:
|
||||
1. navigate: https://openclaw.mva154.duckdns.org/enduro/
|
||||
2. wait: 5000
|
||||
3. click: "#tb-gpx"
|
||||
4. wait: 1000
|
||||
5. click: "#source-btn-nearby"
|
||||
6. wait: 500
|
||||
7. click: "#btn-gpx-find-nearby"
|
||||
8. wait: 4000
|
||||
9. screenshot: "18-external-link-button"
|
||||
10. check-visual: "В каждой карточке .gpx-nearby-card в правом углу видна кнопка «↗» (.gnc-external). Кнопка имеет hover-состояние (cursor:pointer), визуально отличима от основной кнопки «Показать»."
|
||||
3. click: "#terrain-toggle"
|
||||
4. wait: 500
|
||||
5. click: "#public-tracks-cb"
|
||||
6. wait: 3000
|
||||
7. click: "#public-tracks-cb"
|
||||
8. wait: 1500
|
||||
9. screenshot: "20-public-tracks-disabled"
|
||||
10. check-visual: "Чекбокс снят. Все линии публичных треков исчезли с карты. Ссылка «Фильтры…» рядом с чекбоксом скрылась. Базовые слои (схема, trails, POI) остались видимыми и без изменений."
|
||||
|
||||
Reference in New Issue
Block a user