pax f2f7d64759 popout/state: test scaffolding (62 tests, 27 pass at skeleton stage)
Lays down the full test surface for the popout state machine ahead of
any transition logic. 62 collected tests across the four categories
from docs/POPOUT_REFACTOR_PLAN.md "Test plan":

  1. Read-path queries (4 tests, all passing at commit 3 — these
     exercise the parts of the skeleton that are already real:
     compute_slider_display_ms, the terminal Closing guard, the
     initial state defaults)
  2. Per-state transition tests (~22 tests, all failing at commit 3
     because the per-event handlers in state.py are stubs returning
     []. Each documents the expected new state and effects for one
     specific (state, event) pair. These pass progressively as
     commits 4-11 land.)
  3. Race-fix invariant tests (6 tests — one for each of the six
     structural fixes from the prior fix sweep: EOF race, double-
     navigate, persistent viewport, F11 round-trip, seek pin,
     pending mute replay. The EOF race test already passes because
     dropping VideoEofReached in LoadingVideo is just "stub returns
     []", which is the right behavior for now. The others fail
     until their transitions land.)
  4. Illegal transition tests (17 parametrized cases — at commit 11
     these become BOORU_VIEWER_STRICT_STATE-gated raises. At commits
     3-10 they pass trivially because the stubs return [], which is
     the release-mode behavior.)

All 62 tests are pure Python:
  - Import only `booru_viewer.gui.popout.state` and `popout.viewport`
  - Construct StateMachine() directly
  - Use direct field mutation (`m.state = State.PLAYING_VIDEO`) for
    setup, dispatch the event under test, assert the new state +
    returned effects
  - No QApplication, no mpv, no httpx, no filesystem outside tmp_path
  - Sub-100ms total runtime (currently 0.31s including test discovery)

The forcing function: if state.py grows a PySide6/mpv/httpx import,
this test file fails to collect and the suite breaks. That's the
guardrail that keeps state.py pure as transitions land.

Test count breakdown (62 total):
- 4 trivially-passing (read-path queries + initial state)
- 22 transition tests (one per (state, event) pair)
- 6 invariant tests (mapped to the six race fixes)
- 17 illegal transition cases (parametrized over (state, event) pairs)
- 5 close-from-each-state cases (parametrized)
- 8 misc (state field persistence, window events)

Result at commit 3:
  35 failed, 27 passed in 0.31s

The 27 passing are exactly the predicted set: trivial reads + the
illegal-transition pass-throughs (which work today because the stubs
return [] just like release-mode strict-state would). The 35 failing
are the transition handlers that need real implementations.

Phase A test suite (16 tests in tests/core/) still passes — this
commit only adds new tests, no existing test changed.

Test cases for state machine implementation (commits 4-11):
- Each failing test is its own commit acceptance criterion
- Commit N "passes" when the relevant subset of tests turns green
- Final state machine sweep (commit 11): all 62 tests pass
2026-04-08 19:27:23 -05:00
..