mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 22:51:11 +00:00
f7dd6c98d8
ci/woodpecker/push/build-arm Pipeline failed
Security audit (5 phases) → reports under _docs/05_security/. AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts. AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via package.json overrides in both roots; clean reinstall clears all bun audit advisories. Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44, NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report. Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7). 229 PASS / 13 SKIP / 0 FAIL on static + fast suites. Co-authored-by: Cursor <cursoragent@cursor.com>
1658 lines
57 KiB
Markdown
1658 lines
57 KiB
Markdown
# Blackbox Tests
|
||
|
||
Every test is observed at the SPA's public surface — DOM, ARIA, outbound network, EventSource state, browser storage, console — never via `src/` imports beyond the typed wire-contract enums (`P9`). Each test trails an `Expected result source: results_report.md row N` line; the comparison method, tolerance, and reference file for the assertion come from that row. Profile (`fast` / `e2e` / `static`) decides which runner picks the test up — see `environment.md`.
|
||
|
||
## Positive Scenarios
|
||
|
||
### FT-P-01: Bootstrap refresh sends credentials:'include'
|
||
|
||
**Summary**: On `<AuthContext>` init, the bootstrap refresh call includes the HttpOnly refresh cookie.
|
||
**Traces to**: AC-01
|
||
**Category**: Auth — bootstrap
|
||
**Profile**: fast
|
||
|
||
**Preconditions**:
|
||
- Refresh cookie present in the browser jar (test stubs it on the test domain).
|
||
- No bearer in memory.
|
||
|
||
**Input data**: app mount in a fresh browser session — `results_report.md` row 02.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount `<App>` (test renderer or e2e navigation to `/`) | Outbound `fetch` to `/api/admin/auth/refresh` is observed |
|
||
| 2 | Inspect the `RequestInit` of that fetch | `credentials === 'include'`; cookie sent by the test runtime |
|
||
|
||
**Expected outcome**: see row 02 (exact: `credentials: 'include'`).
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 02.
|
||
|
||
---
|
||
|
||
### FT-P-02: 401-retry sequence — refresh and retry the original request
|
||
|
||
**Summary**: A 401 on an authenticated request triggers `POST /api/admin/auth/refresh` (cookie-bound) then a retry of the original request with the new bearer.
|
||
**Traces to**: AC-01, AC-23
|
||
**Profile**: fast (with MSW) and e2e
|
||
|
||
**Preconditions**:
|
||
- Authenticated session active; bearer about to expire.
|
||
|
||
**Input data**: any authenticated `GET /api/admin/*` issued via `apiClient` — `results_report.md` row 03.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Trigger an authenticated call whose first response is 401 (stub) | The SPA issues `POST /api/admin/auth/refresh` with `credentials:'include'` |
|
||
| 2 | Refresh stub returns 200 with new bearer | The SPA retries the original request with the new bearer |
|
||
| 3 | Inspect the final response observed by the calling code | 200 (success) |
|
||
|
||
**Expected outcome**: sequence exactly per row 03; exactly one refresh call per refresh cycle (row 12).
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` rows 03, 12.
|
||
|
||
---
|
||
|
||
### FT-P-03: Refresh transparency — `<ProtectedRoute>` does not unmount
|
||
|
||
**Summary**: Auth refresh occurring mid-session does not unmount the routed view.
|
||
**Traces to**: AC-23
|
||
**Profile**: fast
|
||
|
||
**Preconditions**:
|
||
- User on `/flights`; bearer about to expire.
|
||
|
||
**Input data**: in-flight refresh while `<FlightsPage>` is mounted — `results_report.md` row 11.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount `/flights`; record render counter on `<FlightsPage>` | initial render = 1 |
|
||
| 2 | Force a 401 → refresh → retry cycle | refresh handled |
|
||
| 3 | Inspect render counter post-refresh | delta ≤ 1 re-render |
|
||
|
||
**Expected outcome**: row 11 — `<ProtectedRoute>` children stay mounted; ≤1 re-render delta.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 11.
|
||
|
||
---
|
||
|
||
### FT-P-04: AnnotationStatus enum on the wire
|
||
|
||
**Summary**: Outbound annotation save body's `status` is a number from the spec set `{0,10,20,30,40}`.
|
||
**Traces to**: AC-04
|
||
**Profile**: fast (static + payload assertion)
|
||
|
||
**Preconditions**: `_docs/00_problem/input_data/enum_spec_snapshot.json` is up-to-date (Phase 3 gate).
|
||
|
||
**Input data**: `POST /api/annotations/annotations` body captured at save — `results_report.md` row 18; also static check of `src/types/index.ts` per row 14.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Static read `AnnotationStatus` from `src/types/index.ts` | `{None:0, Created:10, Edited:20, Validated:30, Deleted:40}` |
|
||
| 2 | Trigger an annotation save in `<AnnotationsPage>` (test mount) | `POST /api/annotations/annotations` issued |
|
||
| 3 | Inspect body | `body.status ∈ {0,10,20,30,40}` |
|
||
|
||
**Expected outcome**: rows 14 (enum map) and 18 (wire payload).
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` rows 14, 18.
|
||
|
||
---
|
||
|
||
### FT-P-05: MediaStatus / Affiliation / CombatReadiness enums match the spec
|
||
|
||
**Summary**: Each enum in `src/types/index.ts` matches the spec member set and numeric values pinned in `enum_spec_snapshot.json`.
|
||
**Traces to**: AC-04
|
||
**Profile**: static
|
||
|
||
**Preconditions**: snapshot present (Phase 3 gate).
|
||
|
||
**Input data**: static read of `src/types/index.ts` against the snapshot — `results_report.md` rows 15, 16, 17.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Parse `src/types/index.ts` for `enum MediaStatus`, `Affiliation`, `CombatReadiness` | parsed without error |
|
||
| 2 | Compare member sets to snapshot | sets equal per row |
|
||
| 3 | Compare numeric values member-by-member | equal per row |
|
||
|
||
**Expected outcome**: rows 15, 16, 17 (set_contains + exact).
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` rows 15, 16, 17.
|
||
|
||
---
|
||
|
||
### FT-P-06: Detection wire payload — affiliation + combatReadiness in spec value sets
|
||
|
||
**Summary**: Every `Detection` element of an outbound annotation save has valid enum members.
|
||
**Traces to**: AC-04
|
||
**Profile**: fast
|
||
|
||
**Preconditions**: snapshot present; an annotation with N detections about to be saved.
|
||
|
||
**Input data**: `POST /api/annotations/annotations` body — `results_report.md` row 19.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Save an annotation with `N≥2` detections, mixed affiliations and readiness | request observed |
|
||
| 2 | Inspect every `detections[i]` | `affiliation` and `combatReadiness` are in spec value sets |
|
||
|
||
**Expected outcome**: row 19.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 19.
|
||
|
||
---
|
||
|
||
### FT-P-07: Annotation save endpoint URL is doubly-prefixed
|
||
|
||
**Summary**: Save POSTs to `/api/annotations/annotations`, not the single-prefix path.
|
||
**Traces to**: AC-05
|
||
**Profile**: fast and e2e
|
||
|
||
**Preconditions**: user is editing an annotation in `<AnnotationsPage>`.
|
||
|
||
**Input data**: click Save — `results_report.md` row 22.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Trigger Save | exactly one POST observed |
|
||
| 2 | Inspect URL | matches `^/api/annotations/annotations$` |
|
||
|
||
**Expected outcome**: row 22.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 22.
|
||
|
||
---
|
||
|
||
### FT-P-08: Annotation save body contains all required fields
|
||
|
||
**Summary**: Save body has `{Source, WaypointId, videoTime, mediaId, detections, status}` and NOT the legacy `time` key.
|
||
**Traces to**: AC-05
|
||
**Profile**: fast
|
||
|
||
**Preconditions**: an annotation with all spec-required fields present in the editor state.
|
||
|
||
**Input data**: click Save — `results_report.md` row 23.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Trigger Save with `Source=AI`, a `WaypointId`, a `videoTime` | POST observed |
|
||
| 2 | Inspect body keys | `{Source, WaypointId, videoTime, mediaId, detections, status} ⊆ keys`; `time` absent |
|
||
|
||
**Expected outcome**: row 23.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 23.
|
||
|
||
---
|
||
|
||
### FT-P-09: Annotation-status SSE opens on `<AnnotationsPage>` mount
|
||
|
||
**Traces to**: AC-09
|
||
**Profile**: fast (EventSource test double) and e2e
|
||
|
||
**Input data**: mount `/annotations` — `results_report.md` row 24.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount `/annotations` | exactly one `EventSource` constructed |
|
||
| 2 | Inspect URL | matches `^/api/annotations/annotations/events(\?|$)` |
|
||
|
||
**Expected outcome**: row 24.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 24.
|
||
|
||
---
|
||
|
||
### FT-P-10: Annotation-status SSE closes on unmount
|
||
|
||
**Traces to**: AC-09
|
||
**Profile**: fast
|
||
|
||
**Preconditions**: continuation of FT-P-09.
|
||
|
||
**Input data**: unmount `/annotations` — `results_report.md` row 25.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Unmount the route | `EventSource.readyState → CLOSED (2)` within 1 s |
|
||
|
||
**Expected outcome**: row 25 (CLOSED within ≤ 1 000 ms).
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 25.
|
||
|
||
---
|
||
|
||
### FT-P-11: Sync image detect endpoint
|
||
|
||
**Traces to**: AC-25 (sync path)
|
||
**Profile**: fast and e2e
|
||
|
||
**Input data**: click Detect on a `MediaType.Image` — `results_report.md` row 26.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Open `<AnnotationsSidebar>` on an image; click Detect | exactly one POST observed |
|
||
| 2 | Inspect URL | matches `^/api/detect/[0-9]+$` |
|
||
|
||
**Expected outcome**: row 26.
|
||
**Max execution time**: 30s (server-side detect time included).
|
||
**Expected result source**: `results_report.md` row 26.
|
||
|
||
---
|
||
|
||
### FT-P-12: Async video detect endpoint + SSE (target — Phase B)
|
||
|
||
**Traces to**: AC-25 (async path)
|
||
**Profile**: fast (mocked) — `quarantined` until F7 lands
|
||
**Status**: target — UI does not implement async video detect today (`04_verification_log.md` F7). Test is written so it activates the day the feature ships.
|
||
|
||
**Input data**: click Detect on a `MediaType.Video` (behind feature flag) — `results_report.md` row 27.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Trigger detect on a video | one POST `^/api/detect/video/[0-9]+$` |
|
||
| 2 | Inspect response | JSON `{jobId: <int>}` |
|
||
| 3 | Wait | EventSource opens to `^/api/detect/stream/[0-9]+(\?|$)` |
|
||
|
||
**Expected outcome**: row 27 (3 assertions).
|
||
**Max execution time**: 10s.
|
||
**Expected result source**: `results_report.md` row 27.
|
||
|
||
---
|
||
|
||
### FT-P-13: Long-video detect carries `X-Refresh-Token` header
|
||
|
||
**Traces to**: AC-25
|
||
**Profile**: fast — `quarantined` until F7 lands (and the header is added per Step 4)
|
||
**Status**: target.
|
||
|
||
**Input data**: long-video async detect — `results_report.md` row 28.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Trigger long-video detect | request headers observed |
|
||
| 2 | Inspect headers | `X-Refresh-Token` present and non-empty |
|
||
|
||
**Expected outcome**: row 28.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 28.
|
||
|
||
---
|
||
|
||
### FT-P-14: Overlay membership at the lower in-window edge
|
||
|
||
**Summary**: An annotation at `videoTime = T - 30 ms` (inside `[-50, +150]` window around `T`) renders.
|
||
**Traces to**: AC-28
|
||
**Profile**: fast (component test on `<CanvasEditor>`)
|
||
|
||
**Input data**: `currentTime = T`, annotation at `T - 30 ms` — `results_report.md` row 29.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Render `<CanvasEditor>` with the annotation and set `currentTime = T` | overlay rect rendered |
|
||
|
||
**Expected outcome**: row 29 (range check).
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 29.
|
||
|
||
---
|
||
|
||
### FT-P-15: Overlay membership at the upper in-window edge
|
||
|
||
**Summary**: An annotation at `videoTime = T + 120 ms` renders.
|
||
**Traces to**: AC-28
|
||
**Profile**: fast
|
||
|
||
**Input data**: `currentTime = T`, annotation at `T + 120 ms` — derived from `results_report.md` row 29.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Render `<CanvasEditor>` with the annotation and `currentTime = T` | overlay rendered |
|
||
|
||
**Expected outcome**: present.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 29.
|
||
|
||
---
|
||
|
||
### FT-P-16: Flight selection persists via `PUT /api/annotations/settings/user`
|
||
|
||
**Traces to**: AC-06
|
||
**Profile**: fast
|
||
|
||
**Input data**: `FlightContext.selectFlight(<id>)` — `results_report.md` row 32.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Trigger `selectFlight` for a known flight id | exactly one PUT observed |
|
||
| 2 | Inspect URL | matches `^/api/annotations/settings/user$` |
|
||
| 3 | Inspect body | `{selectedFlightId: <id>}` (subset) |
|
||
| 4 | Code-search the test domain | NO call to `/api/flights/select` |
|
||
|
||
**Expected outcome**: row 32.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 32.
|
||
|
||
---
|
||
|
||
### FT-P-17: Selected-flight rehydration on boot
|
||
|
||
**Traces to**: AC-06
|
||
**Profile**: e2e (and fast with stubbed `UserSettings`)
|
||
|
||
**Preconditions**: `UserSettings.selectedFlightId` known in seed.
|
||
|
||
**Input data**: full reload — `results_report.md` row 33.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Hard reload as the seeded user | app boots |
|
||
| 2 | Inspect `FlightContext` state via DOM (selected flight indicator) | matches `UserSettings.selectedFlightId` |
|
||
|
||
**Expected outcome**: row 33.
|
||
**Max execution time**: 10s.
|
||
**Expected result source**: `results_report.md` row 33.
|
||
|
||
---
|
||
|
||
### FT-P-18: Live-GPS SSE opens within 5 s of flight select
|
||
|
||
**Traces to**: AC-08
|
||
**Profile**: e2e (uses `flights/` live-gps simulator)
|
||
|
||
**Input data**: select a flight — `results_report.md` row 34.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Select a flight in `<Header>` | EventSource opens |
|
||
| 2 | Inspect URL | matches `^/api/flights/[0-9]+/live-gps(\?|$)` |
|
||
| 3 | Wait | `readyState === OPEN (1)` within ≤ 5 000 ms |
|
||
|
||
**Expected outcome**: row 34.
|
||
**Max execution time**: 10s.
|
||
**Expected result source**: `results_report.md` row 34.
|
||
|
||
---
|
||
|
||
### FT-P-19: Live-GPS SSE closes within 1 s of deselect
|
||
|
||
**Traces to**: AC-08
|
||
**Profile**: e2e
|
||
|
||
**Input data**: deselect — `results_report.md` row 35.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Deselect the flight | EventSource state transitions |
|
||
| 2 | Inspect | all `^/api/flights/[0-9]+/live-gps` sources `CLOSED (2)` within ≤ 1 000 ms |
|
||
|
||
**Expected outcome**: row 35.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 35.
|
||
|
||
---
|
||
|
||
### FT-P-20: Bulk-validate request URL and body
|
||
|
||
**Traces to**: AC-07
|
||
**Profile**: fast and e2e
|
||
|
||
**Input data**: select N items, click Validate — `results_report.md` row 36.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Select N items on `<DatasetPage>` | selection state observable |
|
||
| 2 | Click Validate | one POST observed |
|
||
| 3 | Inspect URL + body | `/api/annotations/dataset/bulk-status`; body `{ids: <length-N>, targetStatus: 30}` |
|
||
|
||
**Expected outcome**: row 36 (`targetStatus: 30` after AC-04 fix).
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 36.
|
||
|
||
---
|
||
|
||
### FT-P-21: Bulk-validate UI reflects new status within 2 s
|
||
|
||
**Traces to**: AC-07
|
||
**Profile**: fast
|
||
|
||
**Preconditions**: continuation of FT-P-20.
|
||
|
||
**Input data**: server returns 200 — `results_report.md` row 37.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Stub a 200 response | UI updates |
|
||
| 2 | Inspect each selected row within ≤ 2 000 ms | status badge reads `Validated` |
|
||
|
||
**Expected outcome**: row 37.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 37.
|
||
|
||
---
|
||
|
||
### FT-P-22: i18n key parity en ↔ ua
|
||
|
||
**Traces to**: AC-12
|
||
**Profile**: static
|
||
|
||
**Input data**: `src/i18n/en.json`, `src/i18n/ua.json` — `results_report.md` row 45.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Read both files | parsed |
|
||
| 2 | Compute deep, sorted key sets | `set_equals` |
|
||
|
||
**Expected outcome**: row 45.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 45.
|
||
|
||
---
|
||
|
||
### FT-P-23: No raw user-visible strings outside `t(...)`
|
||
|
||
**Traces to**: AC-12
|
||
**Profile**: static (lint rule)
|
||
|
||
**Input data**: lint over `src/**/*.tsx` — `results_report.md` row 46.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Run the lint rule | reports 0 findings |
|
||
| 2 | Allow-list applies to proper-noun acronyms only | allow-list respected |
|
||
|
||
**Expected outcome**: row 46.
|
||
**Max execution time**: 30s.
|
||
**Expected result source**: `results_report.md` row 46.
|
||
|
||
---
|
||
|
||
### FT-P-24: i18n detector path used at first boot
|
||
|
||
**Traces to**: AC-13
|
||
**Profile**: fast — `quarantined` until the detector is added in Step 4
|
||
|
||
**Input data**: first boot in a clean profile — `results_report.md` row 47.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount the app with a profile providing `Accept-Language: uk` | i18next initialises |
|
||
| 2 | Inspect `i18next.language` | resolves from detector (not hardcoded `'en'`) |
|
||
| 3 | Static read `src/i18n/i18n.ts` | no `lng: 'en'` hardcoded init present |
|
||
|
||
**Expected outcome**: row 47.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 47.
|
||
|
||
---
|
||
|
||
### FT-P-25: i18n persistence across reload
|
||
|
||
**Traces to**: AC-13
|
||
**Profile**: e2e — `quarantined` until the detector + persistence land
|
||
|
||
**Input data**: toggle language, reload — `results_report.md` row 48.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Click language toggle in `<Header>` to `uk` | UI re-renders in Ukrainian |
|
||
| 2 | Hard reload | app boots |
|
||
| 3 | Inspect `i18next.language` | equals `uk` |
|
||
|
||
**Expected outcome**: row 48.
|
||
**Max execution time**: 10s.
|
||
**Expected result source**: `results_report.md` row 48.
|
||
|
||
---
|
||
|
||
### FT-P-26: Class-delete with confirmation — happy path
|
||
|
||
**Traces to**: AC-14, AC-30
|
||
**Profile**: fast
|
||
|
||
**Input data**: click Delete on a class entry in `<AdminPage>` and Confirm — `results_report.md` row 49.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Click Delete | `<ConfirmDialog>` mounts |
|
||
| 2 | Click Confirm | exactly one DELETE observed |
|
||
| 3 | Inspect URL | matches `^/api/admin/classes/[0-9]+$` |
|
||
|
||
**Expected outcome**: row 49 (confirm path).
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 49.
|
||
|
||
---
|
||
|
||
### FT-P-27: Destructive policy — dialog before request for every destructive surface
|
||
|
||
**Traces to**: AC-14
|
||
**Profile**: fast
|
||
|
||
**Input data**: each destructive surface listed in `_docs/ui_design/` (class delete, user deactivate, dataset bulk-overwrite, irreversible bulk) — `results_report.md` row 51.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | For each surface, trigger the destructive action | `<ConfirmDialog>` opens BEFORE any HTTP fires |
|
||
| 2 | Inspect request log up to dialog open | empty for the destructive route |
|
||
|
||
**Expected outcome**: row 51.
|
||
**Max execution time**: 10s.
|
||
**Expected result source**: `results_report.md` row 51.
|
||
|
||
---
|
||
|
||
### FT-P-28: ConfirmDialog has dialog + modal a11y attributes
|
||
|
||
**Traces to**: AC-15
|
||
**Profile**: fast
|
||
|
||
**Input data**: render `<ConfirmDialog open>` — `results_report.md` row 52.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Open the dialog | root element rendered |
|
||
| 2 | Inspect root attributes | `role="dialog"` and `aria-modal="true"` |
|
||
|
||
**Expected outcome**: row 52.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 52.
|
||
|
||
---
|
||
|
||
### FT-P-29: ConfirmDialog focus trap (Tab cycles inside)
|
||
|
||
**Traces to**: AC-15
|
||
**Profile**: fast
|
||
|
||
**Input data**: open dialog and Tab through — `results_report.md` row 53.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Open dialog; focus on first focusable element | initial focus inside dialog |
|
||
| 2 | Tab repeatedly | focus stays inside the dialog subtree; first ↔ last cycles |
|
||
|
||
**Expected outcome**: row 53.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 53.
|
||
|
||
---
|
||
|
||
### FT-P-30: Header flight dropdown closed-state a11y
|
||
|
||
**Traces to**: AC-16
|
||
**Profile**: fast
|
||
|
||
**Input data**: render `<Header>` with dropdown closed — `results_report.md` row 55.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount Header | dropdown trigger rendered |
|
||
| 2 | Inspect attributes | `role="combobox"`, `aria-expanded="false"`, `aria-haspopup="listbox"` |
|
||
|
||
**Expected outcome**: row 55.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 55.
|
||
|
||
---
|
||
|
||
### FT-P-31: Header flight dropdown open-state a11y
|
||
|
||
**Traces to**: AC-16
|
||
**Profile**: fast
|
||
|
||
**Input data**: open the dropdown — `results_report.md` row 56.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Click trigger | dropdown opens |
|
||
| 2 | Inspect | `aria-expanded="true"`; outside-click handler attached (was NOT attached while closed — observable via DOM event listener count or via behavior under a synthetic outside-click while closed: no-op) |
|
||
|
||
**Expected outcome**: row 56.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 56.
|
||
|
||
---
|
||
|
||
### FT-P-32: ProtectedRoute spinner a11y
|
||
|
||
**Traces to**: AC-17
|
||
**Profile**: fast
|
||
|
||
**Input data**: render `<ProtectedRoute>` in loading state — `results_report.md` row 58.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount in loading state | spinner rendered |
|
||
| 2 | Inspect | `role="status"` + non-empty accessible label |
|
||
|
||
**Expected outcome**: row 58.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 58.
|
||
|
||
---
|
||
|
||
### FT-P-33: ProtectedRoute timeout fallback after 10 s
|
||
|
||
**Traces to**: AC-17
|
||
**Profile**: fast (with fake timers)
|
||
|
||
**Input data**: loading exceeds the timeout — `results_report.md` row 59.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount loading; advance fake time to 10 s | timer fires |
|
||
| 2 | Inspect DOM | fallback (retry CTA or error message) present; spinner unmounted |
|
||
|
||
**Expected outcome**: row 59.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 59.
|
||
|
||
---
|
||
|
||
### FT-P-34: Browser-support smoke (Chromium + Firefox)
|
||
|
||
**Traces to**: AC-18
|
||
**Profile**: e2e (manual smoke per AC — no enforcement today)
|
||
|
||
**Input data**: render `/flights`, `/annotations`, `/dataset` on each supported browser — `results_report.md` row 60.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Navigate to each route | page renders |
|
||
| 2 | Inspect console + DOM landmarks | no console errors; main landmark roles present |
|
||
|
||
**Expected outcome**: row 60.
|
||
**Max execution time**: 30s per browser.
|
||
**Expected result source**: `results_report.md` row 60.
|
||
|
||
---
|
||
|
||
### FT-P-35: Mobile bottom-nav variant at 480 px
|
||
|
||
**Traces to**: AC-19
|
||
**Profile**: e2e
|
||
|
||
**Input data**: render at viewport 480×800 — `results_report.md` row 61.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Resize browser to 480×800 | layout reflows |
|
||
| 2 | Inspect DOM | bottom-nav variant present; top-bar variant absent |
|
||
|
||
**Expected outcome**: row 61.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 61.
|
||
|
||
---
|
||
|
||
### FT-P-36: Desktop top-bar variant at 1024 px
|
||
|
||
**Traces to**: AC-19
|
||
**Profile**: e2e
|
||
|
||
**Input data**: render at viewport 1024×768 — `results_report.md` row 62.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Resize to 1024×768 | reflow |
|
||
| 2 | Inspect DOM | top-bar present; bottom-nav absent |
|
||
|
||
**Expected outcome**: row 62.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 62.
|
||
|
||
---
|
||
|
||
### FT-P-37: Panel-width persistence — debounced PUT on resize end
|
||
|
||
**Traces to**: AC-21
|
||
**Profile**: fast — `quarantined` until `useResizablePanel` writes back (Step 4)
|
||
|
||
**Input data**: drag divider — `results_report.md` row 64.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Simulate drag-end on a `useResizablePanel` divider | PUT observed within 1 s (debounce-aware) |
|
||
| 2 | Inspect URL + body | matches `/api/annotations/settings/user`; body contains `panelWidths` key |
|
||
|
||
**Expected outcome**: row 64.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 64.
|
||
|
||
---
|
||
|
||
### FT-P-38: Panel-width rehydration on reload
|
||
|
||
**Traces to**: AC-21
|
||
**Profile**: e2e — `quarantined` until Step 4
|
||
|
||
**Input data**: reload after resize — `results_report.md` row 65.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Resize panels; capture widths W | widths observable |
|
||
| 2 | Reload | app boots |
|
||
| 3 | Re-measure | widths equal W within ± 1 px |
|
||
|
||
**Expected outcome**: row 65.
|
||
**Max execution time**: 15s.
|
||
**Expected result source**: `results_report.md` row 65.
|
||
|
||
---
|
||
|
||
### FT-P-39: Manual bounding-box draw on `<CanvasEditor>`
|
||
|
||
**Traces to**: AC-35
|
||
**Profile**: fast (synthetic pointer events)
|
||
|
||
**Input data**: mousedown(x1,y1) → mousemove(x2,y2) → mouseup with `selectedClassNum = C`, `photoMode = P` — `results_report.md` row 73.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Fire pointer sequence on the canvas | one new local detection appended |
|
||
| 2 | Inspect detection | `classNum == C + P`; `x,y,w,h` normalised within ± 1 px of the drawn rect |
|
||
|
||
**Expected outcome**: row 73.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 73.
|
||
|
||
---
|
||
|
||
### FT-P-40: 8-handle bbox resize
|
||
|
||
**Traces to**: AC-36 (a)
|
||
**Profile**: fast
|
||
|
||
**Input data**: drag each of the 8 handles in turn — `results_report.md` row 74.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | For h ∈ {NW,N,NE,W,E,SW,S,SE}: mousedown on h, drag (dx,dy), mouseup | bbox edges adjacent to h move; opposite edges unchanged |
|
||
| 2 | Force the drag past zero size | resulting bbox has `w>0` and `h>0` (clamped) |
|
||
|
||
**Expected outcome**: row 74.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 74.
|
||
|
||
---
|
||
|
||
### FT-P-41: Ctrl+click multi-select on canvas
|
||
|
||
**Traces to**: AC-36 (b)
|
||
**Profile**: fast
|
||
|
||
**Input data**: Ctrl+click an unselected bbox; second Ctrl+click on the same — `results_report.md` row 75.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Have one bbox already selected | initial selection = {A} |
|
||
| 2 | Ctrl+click bbox B | selection set = {A, B}; prior selection preserved |
|
||
| 3 | Ctrl+click bbox B again | selection set = {A} (toggle off) |
|
||
|
||
**Expected outcome**: row 75.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 75.
|
||
|
||
---
|
||
|
||
### FT-P-42: Ctrl+wheel zoom-around-cursor
|
||
|
||
**Traces to**: AC-36 (c)
|
||
**Profile**: fast
|
||
|
||
**Input data**: Ctrl+wheel at (cx, cy) — `results_report.md` row 76.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Record the world coordinate at (cx, cy) before zoom | W₀ |
|
||
| 2 | Dispatch Ctrl+wheel | zoom level changes |
|
||
| 3 | Inverse-map (cx, cy) → world coordinate after zoom | equals W₀ within ± 1 viewport px |
|
||
|
||
**Expected outcome**: row 76.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 76.
|
||
|
||
---
|
||
|
||
### FT-P-43: Ctrl+drag pan on empty canvas
|
||
|
||
**Traces to**: AC-36 (d)
|
||
**Profile**: fast
|
||
|
||
**Input data**: Ctrl+drag on empty area — `results_report.md` row 77.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Ctrl+drag from (x1,y1) by (dx,dy) | viewport origin translates by `(-dx,-dy)` within ± 1 px |
|
||
| 2 | Inspect bbox state | no bbox created or modified |
|
||
|
||
**Expected outcome**: row 77.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 77.
|
||
|
||
---
|
||
|
||
### FT-P-44: DetectionClasses loads from `/api/annotations/classes`
|
||
|
||
**Traces to**: AC-37 (load path)
|
||
**Profile**: fast
|
||
|
||
**Input data**: mount with a successful response of N classes — `results_report.md` row 78.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount `<DetectionClasses>` | GET observed |
|
||
| 2 | Inspect rendered entries | N entries; active-mode filter applied; no fallback indicator |
|
||
|
||
**Expected outcome**: row 78.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 78.
|
||
|
||
---
|
||
|
||
### FT-P-45: Class hotkey 1–9 selects `classes[(key-1) + P]`
|
||
|
||
**Traces to**: AC-37 (hotkey path)
|
||
**Profile**: fast (synthetic `keydown` on `window`)
|
||
|
||
**Input data**: keys `'1'..'9'`, `photoMode = P`, classes mode-ordered — `results_report.md` row 79.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | For each key k ∈ {1..9}: dispatch `keydown` | `onSelect` fires once |
|
||
| 2 | Inspect arg | `class.id == classes[(k-1) + P].id` |
|
||
| 3 | Inspect rendered list element | label index `i+1` equals `k` |
|
||
|
||
**Expected outcome**: row 79.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 79.
|
||
|
||
---
|
||
|
||
### FT-P-46: Class click path
|
||
|
||
**Traces to**: AC-37 (click path)
|
||
**Profile**: fast
|
||
|
||
**Input data**: click a class entry — `results_report.md` row 80.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Click entry for class C | `onSelect` fires once with `C.id` |
|
||
|
||
**Expected outcome**: row 80.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 80.
|
||
|
||
---
|
||
|
||
### FT-P-47: Fallback class list on API empty/5xx
|
||
|
||
**Traces to**: AC-37 (fallback)
|
||
**Profile**: fast
|
||
|
||
**Input data**: `GET /api/annotations/classes` returns `[]` (or 5xx) — `results_report.md` row 81.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Stub empty response | fallback list rendered |
|
||
| 2 | Inspect rendered IDs | `FALLBACK_CLASS_NAMES × 3 PhotoMode offsets` (set equals `[0..N-1, 20..20+N-1, 40..40+N-1]`) |
|
||
|
||
**Expected outcome**: row 81.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 81.
|
||
|
||
---
|
||
|
||
### FT-P-48: PhotoMode switch — mode set + filter
|
||
|
||
**Traces to**: AC-38 (mode set + filter)
|
||
**Profile**: fast
|
||
|
||
**Input data**: click Winter while `photoMode = 0` — `results_report.md` row 82.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Click Winter | `onPhotoModeChange(20)` fires once |
|
||
| 2 | Inspect rendered class list | filtered to entries with `photoMode == 20` |
|
||
|
||
**Expected outcome**: row 82.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 82.
|
||
|
||
---
|
||
|
||
### FT-P-49: PhotoMode auto-select when prior class no longer valid
|
||
|
||
**Traces to**: AC-38 (auto-select)
|
||
**Profile**: fast
|
||
|
||
**Input data**: switch mode where previously selected class is not in the new filtered set — `results_report.md` row 83.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Pre-select a Regular class | initial state |
|
||
| 2 | Switch to Night (`photoMode = 40`) | `onSelect` fires once with `modeClasses[0].id` |
|
||
|
||
**Expected outcome**: row 83.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 83.
|
||
|
||
---
|
||
|
||
### FT-P-50: yoloId on the wire — `classNum == classId + photoModeOffset`
|
||
|
||
**Traces to**: AC-38 (wire)
|
||
**Profile**: fast
|
||
|
||
**Input data**: save annotation after drawing with C, P — `results_report.md` row 84.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Draw a bbox with `selectedClassNum = C`, `photoMode = P` | local detection appended |
|
||
| 2 | Save | POST observed |
|
||
| 3 | Inspect new detection in body | `classNum == C + P` |
|
||
|
||
**Expected outcome**: row 84.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 84.
|
||
|
||
---
|
||
|
||
### FT-P-51: Tile-split endpoint contract
|
||
|
||
**Traces to**: AC-39 (endpoint)
|
||
**Profile**: fast — `quarantined` until the split action surfaces on the dataset page
|
||
|
||
**Input data**: click Split tile on a dataset item — `results_report.md` row 85.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Trigger Split tile | one POST observed |
|
||
| 2 | Inspect URL + status | `^/api/annotations/dataset/[0-9]+/split$`; 200 JSON |
|
||
|
||
**Expected outcome**: row 85.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 85.
|
||
|
||
---
|
||
|
||
### FT-P-52: YOLO label parser — happy path
|
||
|
||
**Traces to**: AC-39 (parser happy)
|
||
**Profile**: fast
|
||
|
||
**Input data**: `isSplit: true, splitTile: "3 0.5 0.5 0.2 0.2"` — `results_report.md` row 86.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Pass the item through the parser/render path | no exception |
|
||
| 2 | Inspect parsed result | `{classNum:3, cx:0.5, cy:0.5, w:0.2, h:0.2}` |
|
||
|
||
**Expected outcome**: row 86.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 86.
|
||
|
||
---
|
||
|
||
### FT-P-53: `DatasetItem.isSplit` is honored on the dataset list path
|
||
|
||
**Traces to**: AC-39 (dataset list)
|
||
**Profile**: fast
|
||
|
||
**Input data**: `GET /api/annotations/dataset` response contains `isSplit: true` — `results_report.md` row 88.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Stub the response | render no crash |
|
||
| 2 | Inspect that the field is read into the item state (DOM marker present for split items) | field present |
|
||
|
||
**Expected outcome**: row 88.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 88.
|
||
|
||
---
|
||
|
||
### FT-P-54: Tile auto-zoom viewport matches tile rect
|
||
|
||
**Traces to**: AC-40 (viewport)
|
||
**Profile**: fast — `quarantined` (UX missing today)
|
||
|
||
**Input data**: open a `splitTile`-bearing annotation — `results_report.md` row 89.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Double-click the annotation in `<AnnotationsSidebar>` | `<CanvasEditor>` opens to that annotation |
|
||
| 2 | Inspect viewport rect | equals tile rect within ± 1 px per edge |
|
||
|
||
**Expected outcome**: row 89.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 89.
|
||
|
||
---
|
||
|
||
### FT-P-55: Tile-zoom indicator visible while active
|
||
|
||
**Traces to**: AC-40 (indicator)
|
||
**Profile**: fast — `quarantined`
|
||
|
||
**Input data**: tile zoom active → cleared — `results_report.md` row 90.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Activate tile zoom | indicator icon/badge present in canvas chrome |
|
||
| 2 | Clear tile zoom | indicator removed |
|
||
|
||
**Expected outcome**: row 90.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 90.
|
||
|
||
---
|
||
|
||
## Negative Scenarios
|
||
|
||
### FT-N-01: Overlay annotation below the lower bound is NOT rendered
|
||
|
||
**Summary**: At `currentTime = T`, an annotation at `T - 60 ms` (outside `[-50, +150]` ms) is excluded.
|
||
**Traces to**: AC-28
|
||
**Profile**: fast
|
||
|
||
**Input data**: `currentTime = T`, annotation at `T - 60 ms` — `results_report.md` row 30.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Render with the off-window annotation | overlay NOT rendered |
|
||
|
||
**Expected outcome**: row 30 (absent).
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 30.
|
||
|
||
---
|
||
|
||
### FT-N-02: Overlay annotation above the upper bound is NOT rendered
|
||
|
||
**Traces to**: AC-28
|
||
**Profile**: fast
|
||
|
||
**Input data**: `currentTime = T`, annotation at `T + 160 ms` — `results_report.md` row 31.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Render | overlay NOT rendered |
|
||
|
||
**Expected outcome**: row 31.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 31.
|
||
|
||
---
|
||
|
||
### FT-N-03: Authenticated non-admin navigating to `/admin` is redirected to `/flights`
|
||
|
||
**Traces to**: AC-22
|
||
**Profile**: e2e — `quarantined` until role-gate is added (Step 4 / Step 8)
|
||
|
||
**Input data**: log in as `op_alice` (Operator) → navigate `/admin` — `results_report.md` row 08.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Authenticate as Operator | session active |
|
||
| 2 | Navigate to `/admin` | redirect |
|
||
| 3 | Inspect final URL + tree | URL is `/flights`; `<AdminPage>` NOT mounted |
|
||
|
||
**Expected outcome**: row 08.
|
||
**Max execution time**: 10s.
|
||
**Expected result source**: `results_report.md` row 08.
|
||
|
||
---
|
||
|
||
### FT-N-04: Unauthenticated user navigating to `/admin` is redirected to `/login`
|
||
|
||
**Traces to**: AC-22
|
||
**Profile**: e2e
|
||
|
||
**Input data**: no session → navigate `/admin` — `results_report.md` row 09.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Clear cookies and bearer | unauthenticated |
|
||
| 2 | Navigate to `/admin` | redirect |
|
||
| 3 | Inspect final URL | `/login` |
|
||
|
||
**Expected outcome**: row 09.
|
||
**Max execution time**: 10s.
|
||
**Expected result source**: `results_report.md` row 09.
|
||
|
||
---
|
||
|
||
### FT-N-05: Authenticated user without SETTINGS permission navigating to `/settings`
|
||
|
||
**Traces to**: AC-22
|
||
**Profile**: e2e — `quarantined`
|
||
|
||
**Input data**: `op_bob` (no SETTINGS) → `/settings` — `results_report.md` row 10.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Authenticate as op_bob | session active |
|
||
| 2 | Navigate to `/settings` | redirect |
|
||
| 3 | Inspect final URL + tree | `/flights`; `<SettingsPage>` NOT mounted |
|
||
|
||
**Expected outcome**: row 10.
|
||
**Max execution time**: 10s.
|
||
**Expected result source**: `results_report.md` row 10.
|
||
|
||
---
|
||
|
||
### FT-N-06: Upload of 501 MB file surfaces a user-visible 413 error
|
||
|
||
**Traces to**: AC-10
|
||
**Profile**: e2e
|
||
|
||
**Input data**: 501 MB synthetic file via dropzone — `results_report.md` row 39.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Drop 501 MB onto `<MediaList>` | upload request issued |
|
||
| 2 | nginx returns 413 | rejected upstream |
|
||
| 3 | Inspect UI | user-visible error containing the i18n "file too large" string; NO `alert()` |
|
||
|
||
**Expected outcome**: row 39.
|
||
**Max execution time**: 30s.
|
||
**Expected result source**: `results_report.md` row 39.
|
||
|
||
---
|
||
|
||
### FT-N-07: Class-delete Cancel path — NO DELETE request issued
|
||
|
||
**Traces to**: AC-14, AC-30
|
||
**Profile**: fast
|
||
|
||
**Input data**: click Delete → click Cancel — derived from `results_report.md` row 49.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Click Delete | `<ConfirmDialog>` mounts |
|
||
| 2 | Click Cancel | dialog unmounts |
|
||
| 3 | Inspect network log | zero DELETE requests fired |
|
||
|
||
**Expected outcome**: cancel branch of row 49.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 49.
|
||
|
||
---
|
||
|
||
### FT-N-08: Escape on `<ConfirmDialog>` cancels — no destructive request
|
||
|
||
**Traces to**: AC-15
|
||
**Profile**: fast
|
||
|
||
**Input data**: open dialog → press Escape — `results_report.md` row 54.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Open dialog | mounted |
|
||
| 2 | Dispatch `keydown` Escape | dialog unmounts; cancel callback fires exactly once |
|
||
| 3 | Inspect network log | no destructive HTTP fired |
|
||
|
||
**Expected outcome**: row 54.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 54.
|
||
|
||
---
|
||
|
||
### FT-N-09: Header dropdown Escape — close + handler detached
|
||
|
||
**Traces to**: AC-16
|
||
**Profile**: fast
|
||
|
||
**Input data**: open dropdown → press Escape — `results_report.md` row 57.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Open dropdown | open |
|
||
| 2 | Dispatch Escape | dropdown closes |
|
||
| 3 | Inspect attributes | `aria-expanded="false"`; outside-click handler detached |
|
||
|
||
**Expected outcome**: row 57.
|
||
**Max execution time**: 1s.
|
||
**Expected result source**: `results_report.md` row 57.
|
||
|
||
---
|
||
|
||
### FT-N-10: Malformed YOLO label surfaces a user-visible error (no silent swallow, no NaN render)
|
||
|
||
**Traces to**: AC-39
|
||
**Profile**: fast
|
||
|
||
**Input data**: `isSplit: true, splitTile: "garbage"` — `results_report.md` row 87.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Stub the item; render | parse attempted |
|
||
| 2 | Inspect UI | error toast / inline error present |
|
||
| 3 | Inspect rendered overlay | no NaN values, no tile region rendered |
|
||
|
||
**Expected outcome**: row 87.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 87.
|
||
|
||
---
|
||
|
||
### FT-N-11: Numeric field with empty input — no silent zero
|
||
|
||
**Traces to**: AC-26
|
||
**Profile**: fast — `quarantined` until form hygiene fix (Step 4)
|
||
|
||
**Input data**: clear a `09_settings` numeric input and submit — `results_report.md` row 66.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Clear the field | form state observed |
|
||
| 2 | Click Save | form blocks save |
|
||
| 3 | Inspect network log | no PUT fires |
|
||
| 4 | Inspect DOM | submit disabled OR explicit validation error rendered |
|
||
|
||
**Expected outcome**: row 66.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 66.
|
||
|
||
---
|
||
|
||
### FT-N-12: Numeric field with non-numeric input — rejected
|
||
|
||
**Traces to**: AC-26
|
||
**Profile**: fast — `quarantined`
|
||
|
||
**Input data**: type "abc" and submit — `results_report.md` row 67.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Type non-numeric value | state updates |
|
||
| 2 | Click Save | rejected |
|
||
| 3 | Inspect | validation error rendered; no PUT |
|
||
|
||
**Expected outcome**: row 67.
|
||
**Max execution time**: 3s.
|
||
**Expected result source**: `results_report.md` row 67.
|
||
|
||
---
|
||
|
||
### FT-N-13: Settings save with 500 response — `saving` flag reset; error surfaced
|
||
|
||
**Traces to**: AC-27
|
||
**Profile**: fast — `quarantined` until try/finally fix (Step 4)
|
||
|
||
**Input data**: upstream PUT returns 500 — `results_report.md` row 68.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Trigger save | request issued |
|
||
| 2 | Stub responds 500 within 2 s | failure handled |
|
||
| 3 | Inspect within ≤ 2 000 ms | toast / inline error present; `saving === false`; no navigation away |
|
||
|
||
**Expected outcome**: row 68.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 68.
|
||
|
||
---
|
||
|
||
### FT-N-14: Settings save with network failure — try/finally state reset
|
||
|
||
**Traces to**: AC-27
|
||
**Profile**: fast — `quarantined`
|
||
|
||
**Input data**: PUT throws (network drop) — `results_report.md` row 69.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Trigger save | request issued |
|
||
| 2 | Stub throws | rejection handled |
|
||
| 3 | Inspect | `saving === false`; user-visible error present |
|
||
|
||
**Expected outcome**: row 69.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` row 69.
|
||
|
||
---
|
||
|
||
### FT-N-15: `MediaType` magic-literal / magic-string hygiene
|
||
|
||
**Traces to**: AC-29
|
||
**Profile**: static
|
||
|
||
**Input data**: regex sweep of `src/` for `mediaType\s*[!=]==?\s*[0-9]` and `mediaType\s*[!=]==?\s*['"]` — `results_report.md` rows 20, 21.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Run the two regexes | results captured |
|
||
| 2 | Inspect | `match_count == 0` for both |
|
||
|
||
**Expected outcome**: rows 20, 21.
|
||
**Max execution time**: 5s.
|
||
**Expected result source**: `results_report.md` rows 20, 21.
|
||
|
||
---
|
||
|
||
### FT-N-16: mission-planner `getWeatherData` fail-soft when `VITE_OWM_API_KEY` is unset
|
||
|
||
**Traces to**: AC-42 (AZ-499 AC-3)
|
||
**Profile**: fast
|
||
|
||
**Input data**: build-time env with `VITE_OWM_API_KEY=""` (or undefined).
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Spy `globalThis.fetch` | spy installed, no calls yet |
|
||
| 2 | Stub `import.meta.env.VITE_OWM_API_KEY = ""` and invoke `getWeatherData(50, 30)` | resolves |
|
||
| 3 | Inspect return value | `=== null` |
|
||
| 4 | Inspect fetch spy | `mock.calls.length === 0` |
|
||
|
||
**Pass criteria**: function returns `null` AND no outbound HTTP request is made when the API key is unset. Mirrors the AZ-448 fail-soft contract on the main SPA.
|
||
**Max execution time**: 1s (env stub + sync inspection only).
|
||
**Expected result source**: AZ-499 AC-3 (no `results_report.md` row needed — behavioral test, no input data).
|
||
|
||
---
|
||
|
||
## Cycle 2 Additions (Phase B Cycle 2 — Self-hosted satellite tiles + mission-planner OWM hardening)
|
||
|
||
The scenarios below were appended via `/test-spec` cycle-update mode after Phase B Cycle 2 completed (AZ-498 + AZ-499, batch_11). They use the same template shapes as the original spec. Cross-references: AC-41 (satellite tiles), AC-42 (mission-planner OWM env hardening) are the new global ACs added to `traceability-matrix.md`; the underlying task-spec ACs are AZ-498 AC-1..AC-7, AC-9 and AZ-499 AC-1..AC-6 (AZ-498 AC-8 was dropped with explicit user approval per `_docs/03_implementation/batch_11_report.md`; AZ-499 AC-7 is a manual deliverable, not a test).
|
||
|
||
### FT-P-56: Self-hosted satellite tile URL is env-var resolved
|
||
|
||
**Traces to**: AC-41 (AZ-498 AC-1, AC-2)
|
||
**Profile**: fast
|
||
|
||
**Input data**: build-time env with `VITE_SATELLITE_TILE_URL` set, unset, or set with a trailing slash.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}` and call `getTileUrl()` | returns the env value verbatim |
|
||
| 2 | Stub `VITE_SATELLITE_TILE_URL=""` and call `getTileUrl()` | returns `DEFAULT_SATELLITE_TILE_URL` (`http://localhost:5100/tiles/{z}/{x}/{y}`) |
|
||
| 3 | Stub `VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}/` (trailing slash) | returns the value with the trailing slash stripped |
|
||
| 4 | Mount `<FlightMap>` with the env unset; inspect rendered `<TileLayer>` `data-tile-url` | equals `DEFAULT_SATELLITE_TILE_URL` |
|
||
|
||
**Pass criteria**: all four assertions hold. Mirrors the established `getOwmBaseUrl()` / `getApiBase()` env-resolution pattern.
|
||
**Max execution time**: 2s (jsdom render + four stub variations).
|
||
**Expected result source**: AZ-498 AC-1, AC-2 (no `results_report.md` row needed — env-var plumbing, no input data fixture).
|
||
|
||
---
|
||
|
||
### FT-P-57: `<TileLayer crossOrigin="use-credentials">` enables cookie-auth on tile fetches
|
||
|
||
**Traces to**: AC-41 (AZ-498 AC-3); E1 (air-gap-friendly bundle); RID R-Reliability for tile auth
|
||
**Profile**: fast
|
||
|
||
**Input data**: `<FlightMap>` and `<MiniMap>` mounted with the default tile URL.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount `<FlightMap>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
|
||
| 2 | Mount `<MiniMap pointPosition={…}>`; inspect rendered `<TileLayer>` `data-cross-origin` attribute | `=== "use-credentials"` |
|
||
| 3 | (e2e — gated) Issue `GET <VITE_SATELLITE_TILE_URL substituted with /tiles/1/0/0>` from the rendered map; inspect outbound request | `request.credentials === "include"` (browser attaches the same-origin auth cookie) |
|
||
|
||
**Pass criteria**: every `<TileLayer>` the SPA renders carries `crossOrigin="use-credentials"` so the browser sends the satellite-provider cookie on same-origin tile requests. Step 3 e2e is gated by the cross-workspace satellite-provider cookie-auth ticket landing (Step 16 deploy gate).
|
||
**Max execution time**: 2s for steps 1+2 (fast); e2e step is part of `infrastructure.e2e.ts` — bounded by suite-e2e timeout.
|
||
**Expected result source**: AZ-498 AC-3 (no `results_report.md` row — DOM-attribute observable).
|
||
|
||
---
|
||
|
||
### FT-P-58: Classic/satellite map toggle, `mapType` state, and `MiniMap.Props.mapType` are removed
|
||
|
||
**Traces to**: AC-41 (AZ-498 AC-4)
|
||
**Profile**: fast
|
||
|
||
**Input data**: `<FlightMap>` mounted with the default tile URL; `<MiniMap>` mounted with only `pointPosition` (no `mapType` prop).
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Mount `<FlightMap>`; query `screen.queryByRole('button', { name: /satellite|classic/i })` | returns `null` |
|
||
| 2 | Mount `<FlightMap>`; query `screen.getAllByTestId('tile-layer')` | length `=== 1` (no per-mode branching, single layer) |
|
||
| 3 | Compile-time check: instantiate `<MiniMap pointPosition={…}>` without `mapType` | TypeScript `tsc --noEmit -p tsconfig.test.json` succeeds (STC-T1) |
|
||
| 4 | Compile-time check: source-tree grep for any remaining `mapType` reference under `src/features/flights/` | zero hits (compilation error if not — covered by STC-T1) |
|
||
|
||
**Pass criteria**: no toggle button, no `mapType` state, `MiniMap.Props` has no `mapType`. Removal is permanent; the `flights.planner.satellite` i18n key was removed from both `en.json` and `ua.json` in lockstep (i18n key parity preserved via STC-FP22).
|
||
**Max execution time**: 2s (jsdom render + grep).
|
||
**Expected result source**: AZ-498 AC-4.
|
||
|
||
---
|
||
|
||
### FT-P-59: e2e harness exercises the new `/tiles/{z}/{x}/{y}` path
|
||
|
||
**Traces to**: AC-41 (AZ-498 AC-6); E1 (air-gap)
|
||
**Profile**: e2e
|
||
|
||
**Input data**: suite-e2e compose stack up; `tile-stub` configured at `http://tile-stub:8082/tiles/{z}/{x}/{y}`.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | `infrastructure.e2e.ts` AC-2 — issue `GET http://tile-stub:8082/tiles/1/0/0` from the playwright runner | HTTP 200, response is a 256×256 image (JPEG) |
|
||
| 2 | Inspect response headers | `Content-Type: image/jpeg`, `Cache-Control` present, `ETag` present |
|
||
| 3 | Inspect outbound request from the SPA's `<TileLayer>` | URL matches `^http://tile-stub:8082/tiles/\d+/\d+/\d+$` (NOT `/{z}/{x}/{y}.png`, NOT the legacy `/sat/...` Esri shape) |
|
||
| 4 | Inspect `EXTERNAL_HOSTS` route guard | OSM and Esri hosts are NOT in the allow-list (removed during cycle 2 cleanup) |
|
||
|
||
**Pass criteria**: tile fetch shape matches the satellite-provider contract documented at `_docs/02_document/contracts/satellite-provider/tiles.md`. Note: the same-origin cookie-auth path (cookie attached on the actual fetch) is verified once the cross-workspace satellite-provider cookie-auth ticket lands; until then, the e2e profile uses the `tile-stub` which accepts requests without a cookie.
|
||
**Max execution time**: bounded by suite-e2e infrastructure-test timeout (per `e2e/tests/infrastructure.e2e.ts`).
|
||
**Expected result source**: contract at `_docs/02_document/contracts/satellite-provider/tiles.md` v1.0.0; AZ-498 AC-6.
|
||
|
||
---
|
||
|
||
### FT-P-60: mission-planner `getWeatherData` uses env-resolved key + base URL
|
||
|
||
**Traces to**: AC-42 (AZ-499 AC-1, AC-2, AC-4)
|
||
**Profile**: fast
|
||
|
||
**Input data**: build-time env with `VITE_OWM_API_KEY` set + `VITE_OWM_BASE_URL` either set, unset, or set with a trailing slash.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ wind: { speed: 5, deg: 90 } }` | spy installed |
|
||
| 2 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL contains `appid=abc123` AND `units=metric` |
|
||
| 3 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` |
|
||
| 4 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=https://example.test/data/2.5/` (trailing slash); invoke `getWeatherData(50, 30)` | outbound URL starts with `https://example.test/data/2.5/weather?` (slash stripped) |
|
||
| 5 | Stub `VITE_OWM_API_KEY=abc123` + `VITE_OWM_BASE_URL=""`; invoke `getWeatherData(50, 30)` | outbound URL starts with `https://api.openweathermap.org/data/2.5/weather?` (default base) |
|
||
| 6 | Inspect return value on a successful fetch | `=== { windSpeed: 5, windAngle: 90 }` (existing parsed-wind shape preserved) |
|
||
|
||
**Pass criteria**: every outbound URL is reconstructed from env vars; the public `getWeatherData(lat, lon)` signature and `WeatherData` return shape are unchanged. Pairs with the AZ-499 NFR-Compatibility constraint.
|
||
**Max execution time**: 2s (env stubs + fetch-spy assertions; no real network).
|
||
**Expected result source**: AZ-499 AC-1, AC-2, AC-4 (no `results_report.md` row — env-var plumbing).
|
||
|
||
---
|
||
|
||
### FT-P-61: mission-planner `geocodeAddress` uses env-resolved Google API key
|
||
|
||
**Traces to**: AC-43 (AZ-501 AC-1)
|
||
**Profile**: fast
|
||
|
||
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` set to a placeholder string.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Spy `globalThis.fetch` returning a 200 OK with body `{ status: 'OK', results: [{ geometry: { location: { lat, lng } } }] }` | spy installed |
|
||
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz`; invoke `geocodeAddress('Kyiv, Ukraine')` | outbound URL contains `key=env-key-xyz` AND `address=Kyiv%2C%20Ukraine` |
|
||
| 3 | Inspect return value | `=== { lat, lng }` from the mocked response |
|
||
|
||
**Pass criteria**: the outbound URL is reconstructed from the env var; no literal key remains in `mission-planner/src/services/GeocodeService.ts` (defense-in-depth confirmed by STC-SEC1D / NFT-SEC-09b).
|
||
**Max execution time**: 2s.
|
||
**Expected result source**: AZ-501 AC-1.
|
||
|
||
---
|
||
|
||
### FT-N-17: mission-planner `geocodeAddress` fail-soft when `VITE_GOOGLE_GEOCODE_KEY` is unset
|
||
|
||
**Traces to**: AC-43 (AZ-501 AC-3)
|
||
**Profile**: fast
|
||
|
||
**Input data**: build-time env with `VITE_GOOGLE_GEOCODE_KEY` empty / undefined.
|
||
|
||
**Steps**:
|
||
|
||
| Step | Consumer Action | Expected System Response |
|
||
|------|----------------|------------------------|
|
||
| 1 | Spy `globalThis.fetch`; spy `console.warn` | spies installed |
|
||
| 2 | Stub `VITE_GOOGLE_GEOCODE_KEY=''`; invoke `geocodeAddress('anywhere')` | returns `null`; fetch is NOT called; `console.warn` called exactly once with a message containing `VITE_GOOGLE_GEOCODE_KEY` |
|
||
| 3 | Stub `VITE_GOOGLE_GEOCODE_KEY=env-key-xyz` and force `fetch` to reject with `Error('boom')`; invoke `geocodeAddress('anywhere')` | returns `null`; promise does NOT throw |
|
||
|
||
**Pass criteria**: missing-key path is silent-but-warned and never throws; network-error path is silent and never throws — preserves the LeftBoard address-box UX of "Enter does nothing if address is unresolvable".
|
||
**Max execution time**: 2s.
|
||
**Expected result source**: AZ-501 AC-3.
|
||
|
||
---
|
||
|
||
## Notes carried into Phase 3
|
||
|
||
- All tests tagged `quarantined` correspond to features either pending a Step 4 fix (e.g., AC-13 i18n detector, AC-21 panel persistence, AC-22 role-gate, AC-26/27 form hygiene, AC-39 split surface, AC-40 tile zoom) or pending Phase B implementation (AC-11 bundle gate, AC-24 SSE refresh, AC-25 async video, AC-40 tile zoom). The test is written so it activates the day the implementation lands; Phase 3 will surface them for downgrade or accept.
|
||
- FT-P-13 / FT-P-14 / FT-P-37 / FT-P-38 / FT-P-51 / FT-P-54 / FT-P-55 / FT-N-03 / FT-N-05 / FT-N-11 / FT-N-12 / FT-N-13 / FT-N-14 are all on the quarantine list above.
|
||
- Tests that depend on the enum-spec numeric snapshot (`AC-04` rows 15-17) are blocked on populating `enum_spec_snapshot.json` from the suite spec — Phase 3 surfaces this as the blocking input.
|
||
- AC-37 row 79 depends on backend ordering returning `[0..N-1, 20..20+N-1, 40..40+N-1]`. If the seed reveals otherwise, this test fails — fix can land either side per `data_parameters.md`.
|