solitaire_wasm/src/lib.rs — 5 new unit tests (9 total, was 4):
- serialize_from_saved_round_trip: board key matches after JSON round-trip
- undo_reverts_to_prior_state: state + history length restored after undo
- draw_one_advances_waste_by_one: DrawOne takes exactly 1 card from stock
- draw_three_advances_waste_by_three: DrawThree takes up to 3 cards
- debug_apply_move_json_stock_click: JSON DebugMove path via native method
solitaire_server/e2e/tests/game_behaviors.spec.js — 5 new Playwright tests:
- resume overlay shows when localStorage save exists; seed() returns null
until user interacts (before bootstrap completes a game)
- clicking New Game on overlay clears history and starts fresh (0 moves)
- clicking Resume restores saved move history length exactly
- HUD new-game button resets history to 0 and score to 0
- tab-visibility timer: timer freezes during hidden, resumes when visible
(tests the visibilitychange fix from the 500-game UX audit); uses
page.clock.install() to control setInterval without real-time delay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
play_canvas.spec.js covers the window.__FERROUS_DEBUG__ bridge on the
/play route (five tests): bridge availability + seed param, draw3 URL
param, applyLegalMove/undo round-trip, failureReport schema, and
autonomous autoplay invariant batch across 7 seeds.
All tests drive exclusively through the debug bridge — no DOM selectors,
because the Bevy canvas is a single <canvas> element with no HTML
controls.
Also update SESSION_HANDOFF.md to reflect post-v0.35.1 work (10 commits
since 2026-05-18 handoff), new e2e architecture notes, and HiDPI fix doc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
play.html now loads solitaire_wasm.js alongside the Bevy canvas and
exposes the same window.__FERROUS_DEBUG__ object as /play-classic.
The bridge runs an independent SolitaireGame (WASM logic layer) seeded
from ?seed= / ?draw3= URL params; Bevy renders the visual game in
parallel without coupling.
Methods exposed: seed, state, legalMoves, moveHistory, snapshot,
applyLegalMove, applyMove, draw, undo, serialize, fromSaved, newGame,
failureReport, replayPayload, runAutoplay — matching the /play-classic
contract so the shared Playwright harness targets either route without
modification.
cycle_metrics.js: add --route play-classic|play flag (default
play-classic). Routes to /${route}?seed=N. The resume-overlay clear
step is skipped for /play since the Bevy build uses localStorage-backed
WasmStorage, not a #resume-overlay element.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
solitaire_server/e2e/:
- smoke.spec.js: verifies /play-classic loads, exposes window.__FERROUS_DEBUG__
bridge, keyboard parity (Space=draw, U=undo), debug failure report, and
replay payload builder exports schema-v2 moves.
- gameplay_review.spec.js: HUD/controls render check, stock-click + undo
player flow, draw-mode toggle, autonomous play invariant batch, and
cycle-detection regression guard.
- cycle_metrics.js: headless cycle-rate analysis tool; run via
`npm run review:cycles` with configurable policy, game count, and
thresholds. Regression gate baked into package.json scripts.
- playwright.config.js: targets the local server at http://localhost:8080.
- package.json / package-lock.json: @playwright/test 1.60.0.
.gitea/workflows/web-e2e.yml:
- Runs on pushes to solitaire_server/, solitaire_wasm/, solitaire_core/,
or Cargo changes. Starts the server binary, waits for /health, runs
the full Playwright suite, uploads test-results/ on failure.
docs/testing-architecture.md: documents the three-tier test strategy
(unit → Playwright smoke → cycle regression) and the __FERROUS_DEBUG__
bridge contract.
scripts/update_quaternions_deps.sh: helper to bump the Quaternions
registry deps (klondike, card_game) by version and run the full
safety gate including deterministic replay checks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>