F-04: rename gps_health() response fields per tester feedback: - total_tracks → tracks_total - by_activity → tracks_by_activity - recent_pipeline_runs (list) → last_pipeline_run (object | null) Change LIMIT from 10 to 1; fetch single row instead of a list. F-05: rewrite _findGpsInsertPosition with explicit priority order: 1. gpx-layer-* (ET-006 GPX file layers) — highest priority 2. route-* (ET-002 routing layers) Remove old combined find() that lacked clear priority semantics. Add tests/web/gps_tracks.test.js (22 JS unit tests via node:test): - _findGpsInsertPosition priority logic (9 cases) - Filter state management — default state assertions (5 cases) - Color palette mapping and _buildColorExpression (8 cases) Add tests/web/test_gps_tracks.py — Python pytest runner (8 static checks + node --test invocation). Refs: ET-008 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
6.0 KiB
Python
134 lines
6.0 KiB
Python
"""ET-008 — тесты модуля публичных GPS-треков (gps_tracks.js + endpoint).
|
||
|
||
Изменение ET-008 — модуль `src/web/gps_tracks.js` + FastAPI endpoint
|
||
`/api/gps-tracks`. В CI исполняется только `pytest tests/`, поэтому файл
|
||
покрывает фронтендовую часть двумя способами:
|
||
|
||
1. Статические проверки структуры gps_tracks.js — выполняются всегда.
|
||
2. Поведенческие JS unit-тесты — через встроенный тест-раннер Node
|
||
(`node --test`). Если `node` отсутствует — часть помечается `skip`.
|
||
|
||
API-тесты endpoint живут в tests/api/test_gps_tracks_endpoint.py.
|
||
|
||
Запуск JS-тестов напрямую:
|
||
node --test tests/web/gps_tracks.test.js
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import subprocess
|
||
from pathlib import Path
|
||
from shutil import which
|
||
|
||
import pytest
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||
GPS_TRACKS_JS = REPO_ROOT / "src" / "web" / "gps_tracks.js"
|
||
JS_TEST = REPO_ROOT / "tests" / "web" / "gps_tracks.test.js"
|
||
|
||
|
||
def _read(path: Path) -> str:
|
||
assert path.is_file(), f"не найден {path}"
|
||
return path.read_text(encoding="utf-8")
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Статические проверки gps_tracks.js
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
def test_gps_tracks_module_exists():
|
||
"""Модуль src/web/gps_tracks.js присутствует в репозитории."""
|
||
assert GPS_TRACKS_JS.is_file(), "не найден src/web/gps_tracks.js"
|
||
|
||
|
||
def test_gps_tracks_find_insert_position_defined():
|
||
"""_findGpsInsertPosition() объявлена в модуле."""
|
||
js = _read(GPS_TRACKS_JS)
|
||
assert "function _findGpsInsertPosition(" in js, (
|
||
"_findGpsInsertPosition не объявлена в gps_tracks.js"
|
||
)
|
||
|
||
|
||
def test_gps_tracks_find_insert_position_priority_gpx_first():
|
||
"""F-05: поиск gpx-layer-* идёт до route-* (приоритет 1 > приоритет 2)."""
|
||
js = _read(GPS_TRACKS_JS)
|
||
fn_start = js.index("function _findGpsInsertPosition(")
|
||
fn_end = js.index("\n}", fn_start)
|
||
fn_body = js[fn_start:fn_end]
|
||
|
||
gpx_pos = fn_body.find("gpx-layer-")
|
||
route_pos = fn_body.find("route-")
|
||
assert gpx_pos != -1, "gpx-layer- не найден в _findGpsInsertPosition"
|
||
assert route_pos != -1, "route- не найден в _findGpsInsertPosition"
|
||
assert gpx_pos < route_pos, (
|
||
"gpx-layer-* должен проверяться ДО route-* (приоритет 1 > приоритет 2)"
|
||
)
|
||
|
||
|
||
def test_gps_tracks_find_insert_position_no_exact_route_line():
|
||
"""F-05: старый точный match 'route-line' удалён, используется startsWith."""
|
||
js = _read(GPS_TRACKS_JS)
|
||
fn_start = js.index("function _findGpsInsertPosition(")
|
||
fn_end = js.index("\n}", fn_start)
|
||
fn_body = js[fn_start:fn_end]
|
||
assert "l.id === 'route-line'" not in fn_body, (
|
||
"старый точный матч route-line не должен присутствовать (F-05)"
|
||
)
|
||
|
||
|
||
def test_gps_tracks_state_object_defined():
|
||
"""window.gpsTracksLayer инициализируется в модуле."""
|
||
js = _read(GPS_TRACKS_JS)
|
||
assert "window.gpsTracksLayer = {" in js, (
|
||
"gps_tracks.js не инициализирует window.gpsTracksLayer"
|
||
)
|
||
|
||
|
||
def test_gps_tracks_source_colors_defined():
|
||
"""GPS_SOURCE_COLORS объявлен для всех основных источников."""
|
||
js = _read(GPS_TRACKS_JS)
|
||
for src in ("osm", "enduro_russia", "ttrails", "offmaps", "nakarte"):
|
||
assert src in js, f"GPS_SOURCE_COLORS не содержит ключ {src}"
|
||
|
||
|
||
def test_gps_tracks_activity_colors_defined():
|
||
"""GPS_ACTIVITY_COLORS объявлен для всех 7 типов активности."""
|
||
js = _read(GPS_TRACKS_JS)
|
||
for act in ("enduro", "moto", "offroad", "bicycle", "hike", "ski", "other"):
|
||
assert act in js, f"GPS_ACTIVITY_COLORS не содержит ключ {act}"
|
||
|
||
|
||
def test_gps_tracks_build_color_expression_defined():
|
||
"""_buildColorExpression() объявлена в модуле."""
|
||
js = _read(GPS_TRACKS_JS)
|
||
assert "function _buildColorExpression(" in js, (
|
||
"_buildColorExpression не объявлена в gps_tracks.js"
|
||
)
|
||
|
||
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
# Поведенческие JS unit-тесты через Node
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
node_required = pytest.mark.skipif(
|
||
which("node") is None,
|
||
reason="node не установлен — поведенческие JS unit-тесты пропущены",
|
||
)
|
||
|
||
|
||
@node_required
|
||
def test_js_unit_tests_pass():
|
||
"""F-05 + filters + colors: behavioral JS-тесты gps_tracks.js через `node --test`."""
|
||
assert JS_TEST.is_file(), f"не найден JS-тест {JS_TEST}"
|
||
node = which("node")
|
||
result = subprocess.run(
|
||
[node, "--test", str(JS_TEST)],
|
||
capture_output=True,
|
||
text=True,
|
||
cwd=str(REPO_ROOT),
|
||
)
|
||
assert result.returncode == 0, (
|
||
f"JS unit-тесты GPS-треков упали (код {result.returncode}):\n"
|
||
f"{result.stdout}\n{result.stderr}"
|
||
)
|