test(e2e): add Playwright spec for /play Bevy canvas route
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>
This commit is contained in:
+49
-6
@@ -1,16 +1,45 @@
|
|||||||
# Ferrous Solitaire — Session Handoff
|
# 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
|
## 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`
|
- **Latest tag:** `v0.35.1`
|
||||||
- **Working tree:** clean
|
- **Working tree:** clean
|
||||||
- **Build:** `cargo clippy --workspace -- -D warnings` 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
|
### 1. CHANGELOG documentation debt
|
||||||
|
|
||||||
CHANGELOG.md currently ends at v0.33.0. Entries for v0.34.0, v0.35.0, and v0.35.1
|
CHANGELOG.md currently ends at v0.33.0. All post-v0.33.0 work is in git log. Low
|
||||||
are missing. Low priority (git log is authoritative) but worth closing before the
|
priority — git log is authoritative.
|
||||||
next release.
|
|
||||||
|
|
||||||
### 2. Android APK launch verification (Option A)
|
### 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
|
- **Test input-state pitfall:** `MinimalPlugins` has no input-tick system, so
|
||||||
`ButtonInput::just_pressed` state persists across frames unless explicitly cleared
|
`ButtonInput::just_pressed` state persists across frames unless explicitly cleared
|
||||||
with `input.release(key); input.clear()` between updates.
|
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`.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user