|
| 1 | +import { expect, test } from "@playwright/test"; |
| 2 | +const site = "http://127.0.0.1:18081"; |
| 3 | + |
| 4 | +async function open(page, request, game, extra = {}) { |
| 5 | + const response = await request.post("/v1/matches", { data: { game_id: game, seed: 2, include_info: false, ...extra } }); |
| 6 | + expect(response.ok()).toBe(true); |
| 7 | + const { match_id: id } = await response.json(); |
| 8 | + await page.goto(site + "/#/matches/" + id + "?seat=0&hotseat=1"); |
| 9 | + const board = page.getByRole("group", { name: /interactive board/ }); |
| 10 | + await expect(board).toHaveAttribute("data-ready", "true"); |
| 11 | + await expect(page.getByLabel("Move notation")).toBeEnabled(); |
| 12 | + return { id, board }; |
| 13 | +} |
| 14 | +async function ready(page, turn) { |
| 15 | + await expect(page.locator(".turn-counter")).toHaveText("Turn " + turn); |
| 16 | + await expect(page.getByLabel("Move notation")).toBeEnabled(); |
| 17 | +} |
| 18 | +async function cell(board, col, row, n) { |
| 19 | + const canvas = board.locator("canvas"); |
| 20 | + const bounds = await canvas.boundingBox(); |
| 21 | + await canvas.click({ position: { x: (40 + (col + .5) * 520 / n) * bounds.width / 600, y: (25 + (row + .5) * 520 / n) * bounds.height / 600 } }); |
| 22 | +} |
| 23 | + |
| 24 | +test("Phaser loads on demand and Tic-Tac-Toe supports keyboard and legal-only clicks", async ({ page, request }) => { |
| 25 | + const loaded = []; |
| 26 | + page.on("request", request => loaded.push(request.url())); |
| 27 | + await page.goto(site); |
| 28 | + await expect(page.getByRole("heading", { name: /A playground/ })).toBeVisible(); |
| 29 | + expect(loaded.some(url => /phaser\.js/.test(url))).toBe(false); |
| 30 | + const { id, board } = await open(page, request, "tictactoe"); |
| 31 | + await board.focus(); |
| 32 | + await board.press("Enter"); |
| 33 | + await ready(page, 1); |
| 34 | + const submissions = []; |
| 35 | + page.on("request", request => { if (request.method() === "POST" && request.url().endsWith("/actions")) submissions.push(request); }); |
| 36 | + await cell(board, 0, 0, 3); |
| 37 | + await board.press("ArrowDown"); |
| 38 | + await board.press("Enter"); |
| 39 | + await ready(page, 2); |
| 40 | + expect(submissions).toHaveLength(1); |
| 41 | + for (const [turn, keys] of [[3, ["ArrowUp", "ArrowRight"]], [4, ["ArrowDown"]], [5, ["ArrowUp", "ArrowRight"]]]) { |
| 42 | + for (const key of keys) await board.press(key); |
| 43 | + await board.press("Enter"); |
| 44 | + if (turn < 5) await ready(page, turn); |
| 45 | + } |
| 46 | + await expect(page.getByTestId("match-status")).toHaveText("X wins"); |
| 47 | + const final = await (await request.get(`/v1/matches/${id}/state?seat=0`)).json(); |
| 48 | + expect(final.returns).toEqual([1, -1]); |
| 49 | +}); |
| 50 | + |
| 51 | +test("Connect Four keyboard columns follow gravity through a full game", async ({ page, request }) => { |
| 52 | + const { id, board } = await open(page, request, "connect4"); |
| 53 | + for (const [index, key] of ["1", "2", "1", "2", "1", "2", "1"].entries()) { |
| 54 | + await board.press(key); |
| 55 | + if (index < 6) await ready(page, index + 1); |
| 56 | + } |
| 57 | + await expect(page.getByTestId("match-status")).toHaveText("Red wins"); |
| 58 | + const final = await (await request.get(`/v1/matches/${id}/state?seat=0`)).json(); |
| 59 | + expect(final.observation.json.rows[5].slice(0, 2)).toEqual([0, 1]); |
| 60 | + expect(final.observation.json.rows[2][0]).toBe(0); |
| 61 | +}); |
| 62 | + |
| 63 | +test("chess board uses server promotion choices", async ({ page, request }) => { |
| 64 | + const { id, board } = await open(page, request, "chess", { start: { position: "7k/P7/8/8/8/8/8/7K w - - 0 1" } }); |
| 65 | + await cell(board, 0, 1, 8); |
| 66 | + await cell(board, 0, 0, 8); |
| 67 | + await expect(page.locator(".turn-counter")).toHaveText("Turn 0"); |
| 68 | + await board.press("q"); |
| 69 | + await ready(page, 1); |
| 70 | + const final = await (await request.get(`/v1/matches/${id}/state?seat=0`)).json(); |
| 71 | + expect(final.observation.json.pieces.a8).toBe("Q"); |
| 72 | +}); |
| 73 | + |
| 74 | +test("mobile Sudoku protects givens and supports notes and touch digit entry", async ({ page, request }) => { |
| 75 | + await page.setViewportSize({ width: 360, height: 780 }); |
| 76 | + const { id, board } = await open(page, request, "sudoku", { config: { size: 4 }, start: { position: "123434122143432." } }); |
| 77 | + await cell(board, 0, 0, 4); |
| 78 | + await board.press("2"); |
| 79 | + await expect(page.locator(".turn-counter")).toHaveText("Turn 0"); |
| 80 | + await cell(board, 3, 3, 4); |
| 81 | + await board.press("n"); |
| 82 | + await expect(page.getByRole("button", { name: "Notes", exact: true })).toHaveAttribute("aria-pressed", "true"); |
| 83 | + await board.press("1"); |
| 84 | + await ready(page, 1); |
| 85 | + const noted = await (await request.get(`/v1/matches/${id}/state?seat=0`)).json(); |
| 86 | + expect(noted.observation.json.notes[15]).toBe(1); |
| 87 | + await page.getByRole("button", { name: "Notes", exact: true }).click(); |
| 88 | + await page.getByRole("button", { name: "1", exact: true }).click(); |
| 89 | + await expect(page.getByTestId("match-status")).toContainText("completed the puzzle"); |
| 90 | + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); |
| 91 | + const bounds = await board.boundingBox(); |
| 92 | + expect(bounds.width).toBeLessThanOrEqual(360); |
| 93 | +}); |
| 94 | + |
| 95 | + |
| 96 | +test("renderer loading failures preserve playable text controls", async ({ page, request }) => { |
| 97 | + await page.route("**/game-kit/src/board-host.tsx*", route => route.abort()); |
| 98 | + const response = await request.post("/v1/matches", { data: { game_id: "tictactoe", include_info: false } }); |
| 99 | + const { match_id: id } = await response.json(); |
| 100 | + await page.goto(site + "/#/matches/" + id + "?seat=0&hotseat=1"); |
| 101 | + await expect(page.getByText("The visual board is unavailable. Use the text board below.")).toBeVisible(); |
| 102 | + await page.getByLabel("Legal move", { exact: true }).selectOption("r2c2"); |
| 103 | + await ready(page, 1); |
| 104 | +}); |
0 commit comments