# 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 `` 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 `` (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 — `` 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 `` is mounted — `results_report.md` row 11. **Steps**: | Step | Consumer Action | Expected System Response | |------|----------------|------------------------| | 1 | Mount `/flights`; record render counter on `` | 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 — `` 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 `` (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 ``. **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 `` 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 `` 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: }` | | 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 ``) **Input data**: `currentTime = T`, annotation at `T - 30 ms` — `results_report.md` row 29. **Steps**: | Step | Consumer Action | Expected System Response | |------|----------------|------------------------| | 1 | Render `` 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 `` 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()` — `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: }` (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 `
` | 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 `` | selection state observable | | 2 | Click Validate | one POST observed | | 3 | Inspect URL + body | `/api/annotations/dataset/bulk-status`; body `{ids: , 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 `
` 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 `` and Confirm — `results_report.md` row 49. **Steps**: | Step | Consumer Action | Expected System Response | |------|----------------|------------------------| | 1 | Click Delete | `` 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 | `` 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 `` — `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 `
` 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 `` 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 `` **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 `` | 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 `` | `` 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`; `` 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`; `` 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 `` | 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 | `` 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 `` 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`.