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:
funman300
2026-06-02 14:03:25 -07:00
parent 2cf728210e
commit 2b1ad2161a
2 changed files with 130 additions and 6 deletions
@@ -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();
}
});