From 2b1ad2161a00a9b8932988c9a435047741be14c1 Mon Sep 17 00:00:00 2001 From: funman300 Date: Tue, 2 Jun 2026 14:03:25 -0700 Subject: [PATCH] test(e2e): add Playwright spec for /play Bevy canvas route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- SESSION_HANDOFF.md | 55 +++++++++++-- .../e2e/tests/play_canvas.spec.js | 81 +++++++++++++++++++ 2 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 solitaire_server/e2e/tests/play_canvas.spec.js diff --git a/SESSION_HANDOFF.md b/SESSION_HANDOFF.md index f01822c..a3b509d 100644 --- a/SESSION_HANDOFF.md +++ b/SESSION_HANDOFF.md @@ -1,16 +1,45 @@ # Ferrous Solitaire — Session Handoff -**Last updated:** 2026-05-18 — Three leaderboard bugs fixed, tagged v0.35.1. All commits on origin/master. +**Last updated:** 2026-06-02 — Web e2e test suite complete; `/play` canvas bridge added and tested. All commits on origin/master. --- ## Current state -- **HEAD on origin/master:** `8f86d66` (fix: three leaderboard bugs) +- **HEAD:** `play_canvas.spec.js` added (Playwright tests for `/play` Bevy canvas route) - **Latest tag:** `v0.35.1` - **Working tree:** clean - **Build:** `cargo clippy --workspace -- -D warnings` clean -- **Tests:** 1277 passing / 0 failing across the workspace +- **Tests:** 1243 Rust tests passing; Playwright suite in `solitaire_server/e2e/` + +--- + +## What shipped since the last handoff (v0.35.1 → present, 2026-06-02) + +| Commit | Summary | +|--------|---------| +| `64f975e` | 14 cross-platform UX/UI fixes from 500-game audit | +| `763fdb4` | Fix input: hit-test deck at correct position; accept waste click | +| `1cdb78c` | cargo fmt; add analytics domain to CSP | +| `baf524e` | Rebuild Bevy canvas WASM; add SolitaireGame interactive API | +| `9ff0585` | Remove Quaternions registry auth; canvas WASM drift guard | +| `de7ae16` | Delay first-run modal until splash screen despawns | +| `8b736ca` | Debug drag failures (temp logging, removed in next commit) | +| `8b262af` | Clamp wgpu surface to CSS pixels on HiDPI (prevented WASM panic) | +| `d45b7cb` | Add Playwright e2e test suite for web routes | +| `2cf7282` | Add `window.__FERROUS_DEBUG__` bridge to `/play` for automation | + +**Key audit bugs fixed (all 7 from 500-game UX audit):** timer-after-undo, radial-menu clamping, Android resume flash, tab-hidden timer, orphaned tmp files, drag threshold 4→6px, Draw-1 recycle doc comment. + +**HiDPI wgpu fix:** `WindowResolution::default().with_scale_factor_override(1.0)` added to the Bevy canvas app. Root cause was physical pixels (CSS×DPR) exceeding WebGL2's 2048px per-dimension limit on HiDPI displays. + +**E2E test architecture:** three-tier — Rust unit tests → Playwright smoke/review specs → cycle regression gate. Debug bridge contract in `docs/testing-architecture.md`. + +--- + +## What shipped before v0.35.1 + +See git log. CHANGELOG.md currently ends at v0.33.0 (documentation debt, low priority). --- @@ -83,9 +112,8 @@ Three bugs fixed: ### 1. CHANGELOG documentation debt -CHANGELOG.md currently ends at v0.33.0. Entries for v0.34.0, v0.35.0, and v0.35.1 -are missing. Low priority (git log is authoritative) but worth closing before the -next release. +CHANGELOG.md currently ends at v0.33.0. All post-v0.33.0 work is in git log. Low +priority — git log is authoritative. ### 2. Android APK launch verification (Option A) @@ -128,3 +156,18 @@ and wired to `GameStateResource` events. - **Test input-state pitfall:** `MinimalPlugins` has no input-tick system, so `ButtonInput::just_pressed` state persists across frames unless explicitly cleared with `input.release(key); input.clear()` between updates. + +- **`/play` debug bridge design:** `play.html` runs two independent WASM instances in + `Promise.all([bootstrap(), init()])`. `bootstrap()` sets `window.__FERROUS_DEBUG__` + (logic layer via `solitaire_wasm.js`); `init()` starts the Bevy canvas. The bridge + operates its own `SolitaireGame` — moves applied through the bridge do NOT affect + the Bevy visual game. This is intentional for automation/invariant checking. + +- **HiDPI Bevy canvas:** `WindowResolution::default().with_scale_factor_override(1.0)` + is set in the canvas app. Without this, physical pixels exceed WebGL2's 2048px limit + on HiDPI displays, causing an immediate wgpu panic on the first resize event. + +- **`/play-classic` vs `/play` in e2e:** `smoke.spec.js` + `gameplay_review.spec.js` + target `/play-classic` (DOM-heavy game.html); `play_canvas.spec.js` targets `/play` + using only the `__FERROUS_DEBUG__` bridge (no DOM selectors). `cycle_metrics.js` + supports both via `--route play-classic|play`. diff --git a/solitaire_server/e2e/tests/play_canvas.spec.js b/solitaire_server/e2e/tests/play_canvas.spec.js new file mode 100644 index 0000000..752b072 --- /dev/null +++ b/solitaire_server/e2e/tests/play_canvas.spec.js @@ -0,0 +1,81 @@ +const { test, expect } = require("@playwright/test"); + +async function gotoReadyPlay(page, seed = 42, draw3 = false) { + const suffix = draw3 ? "&draw3=" : ""; + await page.goto(`/play?seed=${seed}${suffix}`); + await page.waitForFunction( + () => + typeof window.__FERROUS_DEBUG__ === "object" && + window.__FERROUS_DEBUG__.seed() !== null, + null, + { timeout: 30_000 } + ); +} + +test("play loads and exposes debug bridge", async ({ page }) => { + await gotoReadyPlay(page, 42); + + const seed = await page.evaluate(() => window.__FERROUS_DEBUG__.seed()); + expect(seed).toBe(42); + + const legalMoves = await page.evaluate(() => window.__FERROUS_DEBUG__.legalMoves()); + expect(Array.isArray(legalMoves)).toBeTruthy(); + expect(legalMoves.length).toBeGreaterThan(0); +}); + +test("play respects draw3 URL param", async ({ page }) => { + await gotoReadyPlay(page, 77, true); + + const snap = await page.evaluate(() => window.__FERROUS_DEBUG__.snapshot()); + expect(snap).not.toBeNull(); + expect(snap.draw_mode).toBe("DrawThree"); +}); + +test("play debug bridge apply and undo work", async ({ page }) => { + await gotoReadyPlay(page, 42); + + const baseline = await page.evaluate(() => window.__FERROUS_DEBUG__.moveHistory().length); + + const applied = await page.evaluate(() => window.__FERROUS_DEBUG__.applyLegalMove(0)); + expect(applied?.ok).toBeTruthy(); + + await expect + .poll(() => page.evaluate(() => window.__FERROUS_DEBUG__.moveHistory().length)) + .toBe(baseline + 1); + + const undone = await page.evaluate(() => window.__FERROUS_DEBUG__.undo()); + expect(undone?.ok).toBeTruthy(); + + await expect + .poll(() => page.evaluate(() => window.__FERROUS_DEBUG__.moveHistory().length)) + .toBe(baseline); +}); + +test("play failure report contains replay diagnostics", async ({ page }) => { + await gotoReadyPlay(page, 42); + + const report = await page.evaluate(() => window.__FERROUS_DEBUG__.failureReport()); + expect(report).not.toBeNull(); + expect(typeof report.seed).toBe("number"); + expect(Array.isArray(report.moveHistory)).toBeTruthy(); + expect(Array.isArray(report.legalMoves)).toBeTruthy(); + expect(report.currentState).toBeTruthy(); + expect(report.invariants).toBeTruthy(); +}); + +test("play autonomous autoplay keeps invariants stable across seed batch", async ({ page }) => { + test.setTimeout(120_000); + const seeds = [0, 1, 2, 5, 13, 42, 77]; + + for (const seed of seeds) { + await gotoReadyPlay(page, seed); + const run = await page.evaluate(() => + window.__FERROUS_DEBUG__.runAutoplay({ + maxSteps: 220, + maxVisitsPerState: 2, + policy: "loop_aware", + }) + ); + expect(run.ok, `seed ${seed} failed: ${JSON.stringify(run)}`).toBeTruthy(); + } +});