feat(e2e): add Playwright browser test suite for web routes
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>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
|
||||
test("play-classic loads and exposes debug bridge", async ({ page }) => {
|
||||
await page.goto("/play-classic?seed=42");
|
||||
await page.waitForFunction(() => typeof window.__FERROUS_DEBUG__ === "object");
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
test("keyboard parity: Space draws and U undoes", async ({ page }) => {
|
||||
await page.goto("/play-classic?seed=42");
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
typeof window.__FERROUS_DEBUG__ === "object" &&
|
||||
window.__FERROUS_DEBUG__.seed() !== null
|
||||
);
|
||||
|
||||
const baselineHistoryLen = await page.evaluate(
|
||||
() => window.__FERROUS_DEBUG__.moveHistory().length
|
||||
);
|
||||
|
||||
await page.keyboard.press("Space");
|
||||
await expect
|
||||
.poll(async () => await page.evaluate(() => window.__FERROUS_DEBUG__.moveHistory().length))
|
||||
.toBe(baselineHistoryLen + 1);
|
||||
|
||||
await page.keyboard.press("KeyU");
|
||||
await expect
|
||||
.poll(async () => await page.evaluate(() => window.__FERROUS_DEBUG__.moveHistory().length))
|
||||
.toBe(baselineHistoryLen);
|
||||
});
|
||||
|
||||
test("debug failure report contains replay diagnostics", async ({ page }) => {
|
||||
await page.goto("/play-classic?seed=42");
|
||||
await page.waitForFunction(() => typeof window.__FERROUS_DEBUG__ === "object");
|
||||
|
||||
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("replay payload builder exports schema-v2 moves", async ({ page }) => {
|
||||
await page.goto("/play-classic?seed=42");
|
||||
await page.waitForFunction(() => typeof window.__FERROUS_DEBUG__ === "object");
|
||||
|
||||
await page.keyboard.press("Space");
|
||||
|
||||
await expect
|
||||
.poll(async () => await page.evaluate(() => window.__FERROUS_DEBUG__.replayPayload() !== null))
|
||||
.toBe(true);
|
||||
const payload = await page.evaluate(() => window.__FERROUS_DEBUG__.replayPayload());
|
||||
expect(payload.schema_version).toBe(2);
|
||||
expect(payload.draw_mode).toMatch(/Draw(One|Three)/);
|
||||
expect(payload.mode).toBe("Classic");
|
||||
expect(Array.isArray(payload.moves)).toBeTruthy();
|
||||
expect(payload.moves.length).toBeGreaterThan(0);
|
||||
expect(payload.win_move_index).toBe(payload.moves.length - 1);
|
||||
});
|
||||
Reference in New Issue
Block a user