301 lines
34 KiB
Markdown
301 lines
34 KiB
Markdown
# Active Context — Strava Frontend
|
||
|
||
## Task State
|
||
|
||
- task_id: TASK-INTEG-SECTION
|
||
- status: success
|
||
- summary: **Playwright-интеграционный тест для TASK-SECTION-DURATION-ZOOM: 3 passed. Фиксы 2 багов (setChartRef timing + resetAll programmatic zoom).**
|
||
- next task: —
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-INTEG-SECTION (+TASK-SECTION-BUGS)
|
||
- `../integration/tests/section-duration.spec.ts` — 3 теста (duration row, zoom persist across tabs, reset on all tabs), self-contained (stable owner `itest-section-owner`, API-seed, Chart.js via `canvas.__vueParentComponent.exposed.chart`).
|
||
- **Bug 1 фикс** (`WorkoutItem.vue` `setChartRef` ~1155): ref пушится сразу (без guard `r.chart`), финализация (`collectCharts` + `applySelectedRangeZoom`) через retry `nextTick` (до 20 попыток) — дождётся асинхронной инициализации Chart.js. `nextTick` добавлен в импорт из "vue" (~строка 440).
|
||
- **Bug 2 фикс** (`ChartGroup.ts` `resetAll` ~125): после `resetZoom(chart)` явно `x.min=undefined; x.max=undefined; chart.update("none")` (broadcastZoom писал min/max напрямую, минуя плагин). Дополнительно: на время цикла `resetAll` установлена `this.broadcasting=true` (try/finally) — `resetZoom` триггерит `onZoomComplete`, который иначе повторно разносит ещё-зумированный диапазон на остальные чарты; guard `isBroadcasting()` отбрасывает эти колбэки.
|
||
- `npx playwright test tests/section-duration.spec.ts` → **3 passed** (56s) на docker-стече.
|
||
- `yarn build` (vue-tsc --noEmit + vite) → exit 0.
|
||
- Known: диагностический `tests/chartprobe.spec.ts` не удалён (rm отклонён) — удалить вручную, если мешает.
|
||
|
||
## TASK-INTEG-SECTION: план делегирования
|
||
|
||
**Scope:** 1 новый файл — `../integration/tests/section-duration.spec.ts` + запуск на стеке. Не трогать `src/`.
|
||
|
||
**Что тестируется (3 UX-требования, компонент `src/pages/workouts/components/WorkoutItem.vue`, страница `workout_item`):**
|
||
1. Клик по графику (выделение участка) → в блоке `.area-data` появляется строка «Продолжительность».
|
||
2. Клик по вкладке «Дистанция» → тот же участок остаётся в zoom (не весь путь).
|
||
3. Клик по reset → участок сброшен; после смена вкладки → весь путь (reset на всех вкладках).
|
||
|
||
**Контракты (проверено Архитектором):**
|
||
- Детальная страница (приватная, с графиками + вкладками): маршрут `workout_item` → URL `{FRONTEND}/workouts/workouts/{id}` (родитель `workouts` + вложенный `workouts/:id`). Навигация `waitUntil: "domcontentloaded"` (Yandex map держит соединения).
|
||
- Seed через API (чисто, без UI-dedup): `POST {BACKEND}/api/v0/signup` (200) ИЛИ `POST /api/v0/signin` (fallback на повторном прогоне) → token; `POST /api/v0/attachment/upload` (FormData поле `file`, Bearer) → `{id}`; `POST /api/v0/workouts` `{attachment_id, name}` → `{id}` (400 `ObjectExists` → взять id из `GET /api/v0/workouts`[0]).
|
||
- Файл: `../../backend/tests/fitness_tracker_data_parsing-main/20240507053735.fit` (тот же, что в `workout-upload-flow.spec.ts`).
|
||
- Детальный ответ `GET /api/v0/workouts/{id}` → `{ results: [{ timestamp, longitude, latitude, speed, heart_rate, power, elevation, ... }], workout: {...} }` — из него взять полный диапазон (первая/последняя `timestamp`, длина) для порогов zoom.
|
||
- Chart.js: canvas графиков — `#workout-charts .workout-chart-canvas canvas` (несколько; брать `[0]`). Чертит только если есть метрика (у fixture есть speed). Zoom-состояние: `window.Chart.getChart(canvas).isZoomedOrPanned()` + `chart.options.scales.x.min/max` (broadcastZoom пишет сюда) + `chart.scales.x.min/max`.
|
||
- Клик по графику (выбор участка) — через Chart.js `onClick` опцию: `mouse.down` → `mouse.move` → `mouse.up` в точке внутри chartArea (не drag, чтобы не было drag-zoom). Координаты взять через `chart.chartArea` (left/right/top/bottom) в `page.evaluate`.
|
||
- Кнопки вкладок: по тексту «Время» и «Дистанция» (VaButton). Reset: `#zoom-botton`. Блок данных участка: `.area-data` (заголовок «Данные участка»), строка «Продолжительность».
|
||
|
||
**Структура спе (self-contained, repeatable):**
|
||
- `beforeAll`: проверить стек (frontend `<500`, backend `/api/v0/html_test/login`==200), проверить наличие fixture-файла; seed stable owner (`itest-section-owner@example.com`, idempotent signup/signin) + его workout (upload attachment + create; на 400 — id из списка).
|
||
- 3 независимых `test`'а (каждый: `authedPage(token)` → `openWorkoutDetail(id)` → `expect` canvas visible):
|
||
1. «duration row on section select»: canvas-click → `expect .area-data` visible → `expect` текст «Продолжительность» в `.area-data`.
|
||
2. «section zoom persists across tab switch»: canvas-click (zoom) → assert zoomed (x.min<full_min) → click «Дистанция» (кнопка active/primary) → **assert всё ещё zoomed** (x.min≈, не полный диапазон) → click «Время» → assert zoomed (симметрия).
|
||
3. «reset clears section on all tabs»: canvas-click (zoom) → assert zoomed → click `#zoom-botton` (reset) → assert NOT zoomed → click «Дистанция» → assert NOT zoomed (весь путь).
|
||
|
||
**Замечания:**
|
||
- Не `expect` внутри `page.evaluate` — только `return` данных наружу.
|
||
- `authedPage`: goto → set `localStorage.token` → goto (паттерн из `workout-upload-flow.spec.ts`).
|
||
- Node ≥20: запуск через nvm `export PATH="$HOME/.nvm/versions/node/v22.14.0/bin:$PATH"`.
|
||
- `yarn up` (`docker compose up -d --build`) → ждать backend: `until curl -sf http://localhost:8000/api/v0/html_test/login >/dev/null; do sleep 2; done` → `npx playwright test tests/section-duration.spec.ts`.
|
||
|
||
**Acceptance criteria:**
|
||
1. `../integration/tests/section-duration.spec.ts` создан.
|
||
2. `npx playwright test tests/section-duration.spec.ts` → **3 passed** на запущенном стеке.
|
||
3. Не сломать существующие спе (опционально: `npx playwright test --list`).
|
||
|
||
## TASK-SECTION-DURATION-ZOOM: план делегирования
|
||
|
||
**Scope:** 1 файл — `src/pages/workouts/components/WorkoutItem.vue` (компонент, НЕ страница `src/pages/workouts/WorkoutItem.vue`).
|
||
|
||
**Ключевые якоря (текущее состояние кода):**
|
||
|
||
- `times` (~строка 1061) — сырые ISO-метки времени всех точек (общие для всех метрик).
|
||
- `distances` (prop) — накопительные метры на точку.
|
||
- `activeSection = ref<number | null>(null)` (~строка 661) — номер секции, **зависит от `xAxisMode`** (потому что `sectionBounds` пересчитывается под режим).
|
||
- `computeAreaData(start, end)` (~строка 590) — считает средние по метрикам в блок «Данные участка» (`areaAvgData`).
|
||
- `switchXAxis(mode)` (~строка 1135) — сбрасывает `activeSection = null` (баг п.2).
|
||
- `onZoomComplete` (~строка 873) — `activeSection = null` + `computeAreaData(start,end)`.
|
||
- `onClick` (~строка 930) — выбор/тоггл секции.
|
||
- `resetChartZoom()` (~строка 1449) — reset.
|
||
- `setChartRef` (~строка 1126) — после сбора всех refs делает `group.charts = collectCharts()` (сигнал для re-zoom после remount).
|
||
- `secondsToDuration` — экспортируется из `../Definitions.vue` (не импортирован сейчас — добавить в импорт).
|
||
|
||
**Изменения:**
|
||
|
||
1. Заменить `activeSection: number|null` на `selectedRange: { startIdx: number; endIdx: number } | null` (единственный source of truth, **не зависит от режима** — индексы точек).
|
||
2. В `computeAreaData(start,end)`: добавить `avgData["Продолжительность"] = secondsToDuration(durSec)`, где `durSec = max(0, round((Number(new Date(times[end-1])) - Number(new Date(times[start])))/1000))`. Импортировать `secondsToDuration` из `../Definitions.vue`.
|
||
3. `onClick`: выбрать `selectedRange = {startIdx, endIdx}` по `sectionBounds`; тоггл-off когда `selectedRange` совпадает (startIdx== и endIdx==); выбрать иначе → `selectedRange`, `computeAreaData`, `broadcastZoom`.
|
||
4. `onZoomComplete`: `selectedRange = null` (ручной zoom снимает выбор), `computeAreaData(start,end)` сохранить.
|
||
5. `switchXAxis`: **НЕ** сбрасывать `selectedRange`; сохранить `areaAvgData`/`markedCoordinats` (они режим-независимы). Добавить `applySelectedRangeZoom()`: `labels = xAxisMode==='time' ? times : distances`; `toValue = time? Number(new Date(l)) : Number(l)`; `min=toValue(labels[startIdx])`, `max=toValue(labels[endIdx-1])`; `group.broadcastZoom(min,max)`. Вызвать `applySelectedRangeZoom()` в `setChartRef` после `group.charts = collectCharts()` (re-apply zoom в новом режиме после remount).
|
||
6. `resetChartZoom()`: `selectedRange = null` (чтобы после reset на другой вкладке тоже был весь путь), остальное как есть (`resetAll`, `areaAvgData={}`, `markedCoordinats=[]`).
|
||
|
||
**Почему работает «на всех вкладках»: ** reset обнуляет `selectedRange` → при смене вкладки zoom re-apply вызывается только если `selectedRange` set → после reset всегда весь путь.
|
||
|
||
**Acceptance criteria:**
|
||
|
||
1. `yarn build` (vue-tsc --noEmit) → exit 0.
|
||
2. `yarn lint` → 0 ошибок (или зафиксировать пре-экзистинг системный `tsutils.iterateComments`).
|
||
3. Ручной сценарий (MCP browser): выделить участок → в «Данные участка» есть «Продолжительность» → перейти на вкладку «Дистанция» → та же секция остаётся в zoom (не весь путь) → нажать reset → участок сброшен на всех вкладках.
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-SECTION-DURATION-ZOOM
|
||
|
||
- `src/pages/workouts/components/WorkoutItem.vue`:
|
||
- импорт `secondsToDuration` добавлен в импорт из `../Definitions.vue` (рядом с `formatTime`).
|
||
- `activeSection: number|null` → `selectedRange: { startIdx: number; endIdx: number } | null` (единственный source of truth; индексы точек, не зависят от `xAxisMode`).
|
||
- `computeAreaData(start,end)`: в начале цикла добавлена строка `avgData["Продолжительность"] = secondsToDuration(durSec)`, где `durSec = max(0, round((Number(new Date(times[end-1])) - Number(new Date(times[start])))/1000))`.
|
||
- `onClick`: тоггл-off по совпадению `selectedRange.startIdx===startIdx && selectedRange.endIdx===endIdx`; иначе `selectedRange={startIdx,endIdx}` + `computeAreaData` + `broadcastZoom`.
|
||
- `onZoomComplete`: `selectedRange=null` (ручной zoom снимает выбор).
|
||
- `switchXAxis`: УБРАЛ сброс `activeSection` (выбор сохраняется); `areaAvgData`/`markedCoordinats` не трогаю (режим-независимы). Добавлена `applySelectedRangeZoom()` (labels = time? times : distances; broadcastZoom(min,max)).
|
||
- `setChartRef`: внутри `if (chartRefs.length === chartConfigs.value.length)` после `group.charts = collectCharts()` вызывается `applySelectedRangeZoom()` (re-apply zoom в новом режиме после remount).
|
||
- `resetChartZoom`: `selectedRange=null` (после reset на любой вкладке — весь путь).
|
||
- Verified: `yarn build` (vue-tsc --noEmit) → ✓ built in 7.14s, Done (0 type errors).
|
||
- Verified: `grep activeSection` → 0 совпадений; `grep selectedRange` → 9 совпадений; `grep secondsToDuration` → импорт + использование (2 совпадения).
|
||
- Note: `yarn lint` сломан системно — `tsutils.iterateComments is not a function` на всех 73 файлах (пре-экзистинг, дефект env eslint/typescript-estree, не от этих изменений).
|
||
- Browser verification (MCP ad-hoc): НЕ выполнена — dev server `:5173` и интеграционный стек `:8000` не запущены (не поднимал docker-стек ради ad-hoc проверки). Результат по критериям 1–4.
|
||
- ✅ Architect review: прочитаны все изменённые блоки (импорт 481, `computeAreaData` 599-606, `selectedRange` 670, `onClick` 997-1008, `onZoomComplete` 921, `switchXAxis` 1164-1173, `applySelectedRangeZoom` 1142-1153 + вызов в `setChartRef` 1161, `resetChartZoom` 1479). Корректность подтверждена: `times`/`distances`/`lineCoordinates` одной длины; re-apply zoom после remount через `setChartRef`; после reset `selectedRange=null` → на любой вкладке весь путь.
|
||
|
||
## TASK-PROFILE-ULID-BUG: план делегирования
|
||
|
||
**Scope:** `../backend/` (3 файла фикса + 1 новый тест).
|
||
|
||
**Изменения:**
|
||
|
||
1. `../backend/app/modules/users/domains/profiles.py:32` — `default=str(ULID())` → `default_factory=lambda: str(ULID())` (обязательный фикс).
|
||
2. `../backend/app/modules/users/domains/users.py:20` — та же замена (оборона в глубине).
|
||
3. `../backend/app/modules/users/repositories/profile.py` (`DBProfileRepository.update`, ~106-108) — после `del new_dict["created_at"]` добавить `new_dict.pop("id", None)` (PK не должен участвовать в UPDATE).
|
||
4. Новый тест `../backend/tests/test_api_profile.py` (паттерн `test_api_general.py`, фикстуры `client` + `signin_data` из `conftest.py`):
|
||
- signup → получить `profile_id` (из `data["profile"]["id"]`) + token;
|
||
- `PATCH /api/v0/profiles/{profile_id}` с `{"profile": {"first_name": "New", "surname": "Family"}}` → 200;
|
||
- `GET /api/v0/profiles/{profile_id}` → `profile.first_name == "New"` и `surname == "Family"`;
|
||
- второй PATCH (ещё раз) → 200 + персистится (ловит регрессию «один ULID на процесс»).
|
||
|
||
**Acceptance criteria:**
|
||
|
||
1. `./venv/bin/python -m pytest tests/test_api_profile.py -v` → passed.
|
||
2. `./venv/bin/python -m pytest tests/ -v` → green (зафиксировать diff, если есть пре-экзистинг failing).
|
||
3. `./venv/bin/python -m mypy app/modules/users/ --explicit-package-bases --no-error-summary` → 0 errors.
|
||
4. `ruff check` (по `ruff.toml`) → 0 errors.
|
||
|
||
**Не в scope (follow-up):** обновление `../integration/tests/workout-upload-flow.spec.ts` (добавить assert персистентного имени, убрать comment KNOWN BACKEND BUG) и `../integration/README.md` — отдельная мини-задача после success.
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-PROFILE-ULID-BUG
|
||
|
||
- `../backend/app/modules/users/domains/profiles.py:32` — `default=str(ULID())` → `default_factory=lambda: str(ULID())` (ULID теперь вычисляется на каждый `Profile()`, а не один раз при импорте).
|
||
- `../backend/app/modules/users/domains/users.py:20` — та же замена (оборона в глубине).
|
||
- `../backend/app/modules/users/repositories/profile.py` (`DBProfileRepository.update`) — добавлено `new_dict.pop("id", None)` после `del new_dict["created_at"]` (PK не участвует в UPDATE — defensive).
|
||
- `../backend/tests/test_api_profile.py` — 2 API-теста: (1) PATCH с `{"profile": {"first_name": "New", "surname": "Family"}}` → 200 + GET → персистентность имени/фамилии; (2) повторные PATCH → 200 + GET `first_name == "Second"` (ловит регрессию «один ULID на процесс»).
|
||
- Verified: `./venv/bin/python -m pytest tests/test_api_profile.py -v` → **2 passed**.
|
||
- Verified: `./venv/bin/python -m pytest tests/ -v` → **56 passed, 10 errors** — все 10 errors **пре-экзистинг** (проверено через `git stash`: `NotImplementedError: No support for ALTER of constraints in SQLite dialect` в fixture-setup `test_db_profile.py`/`test_db_workout.py`/`test_db_workout_photos.py` — миграции на SQLite, не связаны с фиксом; до/после идентично).
|
||
- Verified: `./venv/bin/python -m mypy app/modules/users/ --explicit-package-bases --no-error-summary` → **0 errors**.
|
||
- Verified: `./venv/bin/python -m ruff check app/modules/users/ tests/test_api_profile.py` → **All checks passed** (полный `ruff check` по репо: единственная находка F401 в `migration/versions/2025-09-25-0000-...-hide-start-end.py:8` — пре-экзистинг, коммит `ff5f468`, вне scope).
|
||
- Follow-up (отдельная задача): update `../integration/tests/workout-upload-flow.spec.ts` (assert персистентного имени вместо KNOWN BACKEND BUG comment) + `../integration/README.md`.
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-TEST-WORKOUT-UPLOAD
|
||
|
||
- `../integration/tests/workout-upload-flow.spec.ts` создан: 5 последовательных тестов (signup UI+профиль, upload .fit + resolve id, make public→GET public=200, hide_start_end→authed GET body true, add photo→img в `.workout-photos-grid`).
|
||
- Ключевое решение: **стабильный owner** (`itest-workout-owner@example.com`, idempotent signup/signin) для Части 2 — dedup по content-hash ГЛОБАЛЕН, а `GET /api/v0/workouts` scoped по `created_by`, поэтому уникальный юзер на повторном прогоне всегда падал бы в dedup-ветку с пустым своим списком. Part 1 (профиль) — уникальный юзер через UI (genuine signup).
|
||
- Детальный URL `/workouts/workouts/:id` (сверено с `router/index.ts`: вложенный `workouts` → `workouts/:id`); навигация `domcontentloaded` (Yandex map держит соединения, `networkidle` не стабилизируется).
|
||
- `../integration/README.md` обновлён (секция + notes про dedup и про backend-баг имени).
|
||
- Verified: `npx playwright test tests/workout-upload-flow.spec.ts` → **5 passed**, стабильно ×3 (dedup-путь) + прогон на свежей DB (redirect-путь). `yarn build` (vue-tsc --noEmit) exit 0.
|
||
- Cleanup: диагностический `../integration/tests/_diag.spec.ts` удалён; `npx playwright test --list` → 16 tests в 5 файлах, `_diag` отсутствует.
|
||
|
||
## ⚠️ Discovered (NOT fixed, out-of-scope test-only)
|
||
|
||
- **Backend-баг сменy имени:** `PATCH /api/v0/profiles/{id}` (ветка `profile`) возвращает 500. Причина: `../backend/app/modules/users/domains/profiles.py:32` — `id: str = field(default=str(ULID()), ...)` вычисляет ULID ОДИН раз при импорте (не `default_factory`), ORM-UPDATE переписывает PK → `IntegrityError: duplicate key (profiles_pkey)`. SPA показывает toast синхронно (до чтения ответа), поэтому имя НЕ персистится. Spec асертит SPA-toast (реальное UI-поведение), персистентное имя задокументировано, но не asserted.
|
||
- **Стек должен быть свежим:** колонка `hide_start_end` появляется только после `alembic upgrade head`. Перед прогоном спе — свежий стек / `docker compose up -d --force-recreate backend`.
|
||
- **`yarn lint` сломан системно** — `tsutils.iterateComments is not a function` (пре-экзистинг, падает и на нетронутые `.ts`, дефект env eslint/typescript-estree).
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-3-FIX
|
||
|
||
- `src/pages/workouts/components/WorkoutItem.vue`:
|
||
- импорт `import { useI18n } from "vue-i18n"` + `const { t } = useI18n();` (рядом с `useToast`).
|
||
- template: чекбокс `hide_start_end` (v-model + `v-on:change="changeHideStartEnd(...)"`) после блока `is_public`.
|
||
- script: `changeHideStartEnd(value: boolean)` → `axiosAuth.patch('/api/v0/workouts/${workoutItem.id}', { hide_start_end: value })` (с guard `!workoutItem` и catch-тостом, как у `changePublic`).
|
||
- i18n: ключ `workout.hide_start_end` добавлен во ВСЕ 6 локали (`br, cn, es, gb, ir, ru`) — ранее отсутствовал (секция `workout` была создана заново).
|
||
- Verified: `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite ✓).
|
||
- Verified: `grep hide_start_end` → 4 совпадения; `grep useI18n` → 2 совпадения.
|
||
- Note: `yarn lint` сломан системно — `tsutils.iterateComments is not a function` на всех .vue файлах (существующий дефект окружения, не от этих изменений).
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-T1
|
||
|
||
- `../backend/tests/test_api_workout.py`: добавлен `test_workout_hide_start_end` — проверяет PATCH `hide_start_end` (False→True→False) с GET-верификацией на каждом шаге.
|
||
- Verified: `./venv/bin/python -m pytest tests/test_api_workout.py::test_workout_hide_start_end -v` → **PASSED (1.92s)**
|
||
- Verified: `./venv/bin/python -m pytest tests/test_api_workout.py -v` → **4 passed (5.13s)**
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-B3
|
||
|
||
- `../backend/app/web/v0/workout.py`: `WorkoutEditReq` + поле `hide_start_end: bool | None = Field(None)`; endpoint передаёт `hide_start_end=req.hide_start_end` в `workout_service.workout_edit(...)`.
|
||
- `../backend/app/modules/charts/services/workout.py`: `workout_edit` + параметр `hide_start_end: bool | None = None`; тело: `if hide_start_end is not None: params["hide_start_end"] = hide_start_end`.
|
||
- Verified: `./venv/bin/python -c "from app.web.v0.workout import workout_update; print('OK')"` → OK.
|
||
- Verified: `./venv/bin/python -m mypy app/web/v0/workout.py app/modules/charts/services/workout.py --explicit-package-bases --no-error-summary` → 0 errors.
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-B2
|
||
|
||
- `../backend/migration/versions/2025-09-25-0000-a1b2c3d4e5f6-hide-start-end.py` создан: `revision='a1b2c3d4e5f6'`, `down_revision='b1c2d3e4f5a6'`; `upgrade()` → `op.add_column('workouts', sa.Column('hide_start_end', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False))`; `downgrade()` → `op.drop_column(...)`. Без `IS_TEST`-guard (колонка нужна и в тестах).
|
||
- Verified: `python3 -c "import ast; ast.parse(...)"` → OK.
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-4
|
||
|
||
- `src/pages/workouts/WorkoutPublicItem.vue`: импорт `trimTrack`; в `.then()` при `d.workoutItem.hide_start_end === true` — обрезка `lineCoordinates`/`distances`/`data` через `trimTrack(..., totalDist)` (totalDist = последний элемент `distances`). `mapCenter` остаётся неизменным.
|
||
- `src/pages/workouts/Definitions.vue`: добавлено поле `hide_start_end: boolean` в тип `WorkoutItem` (восстановлено — было потеряно при git stash).
|
||
- Приватный вид (`WorkoutItem.vue` страница) НЕ затронут.
|
||
- Verified: `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite ✓ 6.86s).
|
||
- Note: `yarn lint` → системная ошибка `tsutils.iterateComments is not a function` (касается ВСЕХ .vue файлов, не связано с изменениями — существующий дефект окружения lint-депенденций).
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-TEST-AUTH
|
||
|
||
- `../integration/tests/auth-flows.spec.ts` создан: 4 теста (login success, login failure, signup+auto-login, logout)
|
||
- Self-seed via `POST /api/v0/signup`, Vuestic selectors (`input[type=email]`, `input[type=password]`)
|
||
- Verified: `npx playwright test tests/auth-flows.spec.ts` → **4 passed (19.2s)**
|
||
|
||
## TASK-TEST-PUBLIC: public-pages.spec.ts
|
||
|
||
**Цель:** Playwright spec для auth-сценариев (signup, login success/error, logout, validation).
|
||
|
||
**Файл для создания:** `../integration/tests/auth-flows.spec.ts`
|
||
|
||
**Конвенции (см. SKILL.md):**
|
||
|
||
- Self-seed user via `POST /api/v0/signup` in `beforeAll`
|
||
- Selectors: `input[type=email]`, `input[type=password]`, `getByRole('button', { name: 'Вход' })`
|
||
- Vuestic floating labels → select by input type, NOT placeholder
|
||
- Auth mock via `page.evaluate(() => localStorage.setItem(...))`
|
||
- Do NOT call `expect()` inside `page.evaluate()`
|
||
- Base URLs: `FRONTEND` (5173), `BACKEND` (8000)
|
||
- `freshPage()` helper: goto → clear localStorage → goto again
|
||
|
||
**Тесты в spec:**
|
||
|
||
1. **Signup**: `POST /api/v0/signup` → returns token, user, profile; navigate to `/auth/signup` → fill form → submit → redirect to `/explore`
|
||
2. **Login success**: pre-seed user via API → UI login → token in localStorage → redirect to `/explore`
|
||
3. **Login failure**: wrong password → error toast appears, stays on `/auth/login`
|
||
4. **Logout**: logged in → open profile dropdown → «Выход» → redirect to login, token cleared
|
||
|
||
**Acceptance Criteria:**
|
||
|
||
1. Spec file created at `../integration/tests/auth-flows.spec.ts`
|
||
2. All tests self-contained (self-seed data)
|
||
3. Follows existing `sidebar-auth-switch.spec.ts` patterns
|
||
4. `npx playwright test tests/auth-flows.spec.ts` passes against integration stack
|
||
|
||
## TASK-MCP-SETUP: Playwright MCP configuration
|
||
|
||
**Что сделано:**
|
||
|
||
- MCP-конфиг Zoo Code (`mcp_settings.json`): server `web-browser` → `@playwright/mcp@latest`, Node 22 via nvm absolute path, `--headless`
|
||
- Chromium `1234` уже установлен в `~/.cache/ms-playwright/`
|
||
- `.mcp.json` создан в проекте (project-level, для совместимости)
|
||
- `techContext.md` обновлён: секция "UI Verification (MCP Browser)"
|
||
|
||
**Как использовать (workflow):**
|
||
|
||
1. `yarn dev` — запустить dev server (порт 5173)
|
||
2. `browser_navigate` → `http://localhost:5173/explore` — проверить публичную страницу
|
||
3. `browser_evaluate` → `() => { localStorage.setItem('token', 'test'); localStorage.setItem('user', JSON.stringify({id:1,name:'Test'})); location.reload(); }` — сымитировать auth
|
||
4. `browser_navigate` → проверить auth-страницы (`/workouts`, `/workouts/:id`)
|
||
5. `browser_snapshot` — проверить DOM-структуру
|
||
6. `browser_take_screenshot` — визуальная проверка
|
||
7. `browser_network_requests` — проверить API-запросы
|
||
|
||
### Баг (контекст)
|
||
|
||
Сайдбар показывает `publicRoutes` (Лента + Маршрут) после логина до F5. Корень: [`NavigationRoutes.ts:70`](src/components/sidebar/NavigationRoutes.ts:70) — `routes` вычисляется один раз при module evaluation и кэшируется. TASK-AUTH-NAV-1 уже добавил реактивный `isAuthenticated` в `useGlobalStore` (инициализация из localStorage + `setAuthenticated` в Login/Logout). Эта задача переключает UI на computed.
|
||
|
||
### TASK-AUTH-NAV-2: реактивные маршруты (3 файла)
|
||
|
||
**1. `src/components/sidebar/NavigationRoutes.ts`**
|
||
|
||
- Сделать named exports: `export const authRoutes: INavigationRoute[]` и `export const publicRoutes: INavigationRoute[]` (сейчас это локальные `const`, не экспортированные).
|
||
- Убрать статическое поле `routes` из default export (или оставить default без `routes`, т.к. оба потребителя перейдут на computed). `INavigationRoute` интерфейс — не трогать.
|
||
- Важно: массивы `authRoutes`/`publicRoutes` остаются теми же по структуре/содержимому — только экспортируются.
|
||
|
||
**2. `src/components/sidebar/AppSidebar.vue`** (Options API, `setup()`)
|
||
|
||
- `import { useGlobalStore } from "../../stores/global-store";` + `import { authRoutes, publicRoutes } from "./NavigationRoutes";` (импорт `navigationRoutes` по default можно оставить/убрать — но все обращения `navigationRoutes.routes` заменить).
|
||
- В `setup()`: `const globalStore = useGlobalStore();`
|
||
- computed: `const routes = computed(() => globalStore.isAuthenticated ? authRoutes : publicRoutes);`
|
||
- В `return` заменить `navigationRoutes` на `routes` (тепловой шаблон уже использует `navigationRoutes.routes` → поправить на `routes.value`? НЕТ — в Options API computed из setup доступно в шаблоне как `routes` без `.value`, т.е. в шаблоне `navigationRoutes.routes` → `routes`).
|
||
- `setActiveExpand`: `navigationRoutes.routes.map(...)` → `routes.value.map(...)` (в setup `.value` нужен).
|
||
- Шаблоны: `v-for="(route, index) in navigationRoutes.routes"` → `in routes`.
|
||
|
||
**3. `src/components/app-layout-navigation/AppLayoutNavigation.vue`** (script setup)
|
||
|
||
- `import { authRoutes, publicRoutes } from "../sidebar/NavigationRoutes";` + `import { useGlobalStore } from "../../stores/global-store";`
|
||
- `const globalStore = useGlobalStore();`
|
||
- `findRouteName` (строка ~64): `traverse(NavigationRoutes.routes)` → `traverse(globalStore.isAuthenticated ? authRoutes : publicRoutes)`.
|
||
- Убедиться, что `items` computed (зависит от `route.matched` и `findRouteName`) остаётся реактивным — т.к. `findRouteName` читает `globalStore.isAuthenticated` внутри `items` computed, реactivity сохранится.
|
||
|
||
### Acceptance Criteria (TASK-AUTH-NAV-2)
|
||
|
||
1. `yarn lint` → 0 ошибок.
|
||
2. `yarn build` → 0 ошибок (vue-tsc --noEmit).
|
||
3. `NavigationRoutes.ts`: `authRoutes` и `publicRoutes` экспортируются; статического `routes` с localStorage-тернарником больше нет.
|
||
4. `AppSidebar.vue`: routes = computed от `globalStore.isAuthenticated`; `setActiveExpand` использует `routes.value`.
|
||
5. `AppLayoutNavigation.vue`: `traverse` использует выбор по `globalStore.isAuthenticated`.
|
||
6. **Ручной сценарий (ключевой)**: без F5 — выход из профиля → `/explore` → логин → сайдбар **сразу** показывает «Мои тренировки» (authRoutes). Аут → сайдбар сразу `publicRoutes`.
|
||
|
||
## ✅ CODER SUCCESS REPORT — TASK-AUTH-NAV-2
|
||
|
||
- `src/components/sidebar/NavigationRoutes.ts`: `export const authRoutes` / `export const publicRoutes`; удалён статический `routes: localStorage.getItem("token") ? ...` из default export (оставлен только `root`).
|
||
- `src/components/sidebar/AppSidebar.vue`: импорт `authRoutes, publicRoutes` + `useGlobalStore`; `computed(() => globalStore.isAuthenticated ? authRoutes : publicRoutes)`; `setActiveExpand` использует `routes.value.map`; шаблон `v-for="(route, index) in routes"`.
|
||
- `src/components/app-layout-navigation/AppLayoutNavigation.vue`: `const globalStore = useGlobalStore()`; `traverse(globalStore.isAuthenticated ? authRoutes : publicRoutes)` внутри `findRouteName` (вызывается в `items` computed → реactivity сохранена).
|
||
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite ✓ 7.07s).
|
||
|
||
## История (закрытые задачи)
|
||
|
||
- **TASK-HIDE-TRACK-4**: success — `trimTrack` в `WorkoutPublicItem.vue` при `hide_start_end`; `hide_start_end` в типе `WorkoutItem` (Definitions.vue). Verified: `yarn build` 0. (Пред. active: TASK-TEST-PUBLIC → в очереди TASK-TEST-PROTECTED.)
|
||
|
||
- **TASK-AUTH-NAV-1**: success — `isAuthenticated` + `setAuthenticated` в `useGlobalStore`; Login `setAuthenticated(true)`, Logout `setAuthenticated(false)`. Verified lint/build 0.
|
||
- TASK-SSR-DETAIL: success — SSR /public/workouts/:id под DOM WorkoutItem.vue.
|
||
- TASK-404: success — HTTP 404 для неизвестных страниц.
|
||
- TASK-PUBLIC-ROUTE-FIX: success — workout_public_item в top-level router.
|
||
- TASK-F11/F12/F13/F14: success — SEO + фиксы карты тренировки.
|