Files
ui/_docs/02_document/tests/blackbox-tests.md
T
Oleksandr Bezdieniezhnykh 510df68bcf [AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
Captures the full output of autodev existing-code Phase A through
Step 4 (Code Testability Revision) for the Azaion UI workspace:

- Step 1 Document: _docs/02_document/ (FINAL_report, architecture,
  glossary, components/, modules/, diagrams/, system-flows,
  module-layout) plus _docs/00_problem/ + _docs/01_solution/ +
  _docs/legacy/ + _docs/how_to_test + README.
- Step 2 Architecture Baseline: architecture_compliance_baseline.md.
- Step 3 Test Spec: _docs/02_document/tests/ (environment,
  test-data, blackbox/performance/resilience/security/
  resource-limit tests, traceability-matrix), enum_spec_snapshot,
  expected_results/results_report.md (98 rows), plus the
  run-tests.sh + run-performance-tests.sh runners.
- Step 4 Code Testability Revision: 01-testability-refactoring/
  run dir (list-of-changes C01-C07, deferred_to_refactor,
  analysis/research_findings + refactoring_roadmap) and the 7
  child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/
  plus _dependencies_table.md.
- _docs/_autodev_state.md pins the cursor at Step 4 / refactor
  Phase 4 entry so /autodev resumes cleanly.

Epic AZ-447 (UI testability gates) tracks the 7 child tasks that
will land in subsequent commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:38:49 +03:00

1479 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 19 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.
---
## 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`.