import { test, expect, type Page } from "@playwright/test"; import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; const romPath = resolve(process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"); test.beforeEach(() => test.skip(!existsSync(romPath), "Requires the local user ROM.")); async function start( page: Page, fixture: "questFixtureWorld" | "questRivalFixtureWorld" | "questLabFixtureWorld", ) { await page.route("**/src/game/runtime.ts", async (route) => { const response = await route.fetch(); const source = await response.text(); const needle = "const world = new GameWorld(assets);"; expect(source).toContain(needle); await route.fulfill({ response, body: source.replace(needle, `const { ${fixture} } = await import('/tests/quest-fixture.ts'); const world = ${fixture}(assets);`) }); }); await page.clock.install({ time: new Date("2026-09-22T12:00:00") }); await page.goto("/"); await page.locator("#rom-file").setInputFiles(romPath); await expect(page.locator("#status")).toContainText("verified"); await page.clock.pauseAt(new Date("2026-09-22T12:05:00")); await page.locator("#play").click(); } async function step(page: Page, direction: string) { await page.keyboard.down(`Arrow${direction}`); await page.clock.runFor(70); await page.keyboard.up(`Arrow${direction}`); await page.clock.runFor(300); } async function drive(page: Page, name = "") { const messages: string[] = []; for (let guard = 0; guard < 180; guard++) { if (await page.locator("#battle").isVisible()) { messages.push(await page.locator("#battle-text").innerText()); const menu = page.locator("#battle-menu"); const fight = menu.getByRole("button", { name: "FIGHT", exact: true }); if (await fight.count()) { await fight.click(); await menu.getByRole("button", { name: /SCRATCH/ }).click(); } else if (await menu.getByRole("button", { name: /Continue/ }).count()) { await menu.getByRole("button", { name: /Continue/ }).click(); } else await page.clock.runFor(1200); } else if (await page.locator("#dialogue-choice input").isVisible()) { messages.push(await page.locator("#dialogue-text").innerText()); const input = page.locator("#dialogue-choice input"); await expect(input).toBeFocused(); await input.fill(name); await input.press("Enter"); await page.clock.runFor(500); } else if (await page.locator("#native-dialogue").isVisible()) { messages.push(await page.locator("#dialogue-text").innerText()); await page.keyboard.press("KeyX"); await page.clock.runFor(500); } else if (await page.locator("#game-screen").getAttribute("data-activity") !== "idle") { await page.clock.runFor(500); } else { // A scene script starts a tick after the tick that ends a warp, so a // single idle observation is not the end of the scene. await page.clock.runFor(200); if ( (await page.locator("#game-screen").getAttribute("data-activity")) !== "idle" || (await page.locator("#native-dialogue").isVisible()) ) continue; await expect(page.locator("#game-notice")).toHaveText(""); return messages.join("\n"); } } throw new Error("Quest scene did not finish."); } async function backup(page: Page) { await page.locator("#tab-saves").click(); const downloaded = page.waitForEvent("download"); await page.locator("#backup").click(); const path = (await (await downloaded).path())!; const record = JSON.parse(readFileSync(path, "utf8")); return { path, state: JSON.parse(Buffer.from(record.state, "base64").toString("utf8")) }; } test("Mr. Pokémon's ROM scene grants the Egg and Pokédex, heals and survives reload", async ({ page }) => { await start(page, "questFixtureWorld"); await step(page, "Up"); await expect(page.locator("#game-screen")).toHaveAttribute("data-map", "26:10"); const shown = await drive(page); expect(shown).toContain("MYSTERY EGG"); expect(shown).toContain("POKéDEX"); const { path, state } = await backup(page); expect(state.events).toContain(30); expect(state.bag.keyItems).toContain(0x45); expect(state.scenes).toMatchObject({ "26:10": 1, "26:3": 1, "24:5": 3 }); expect(state.party[0].hp).toBe(state.party[0].stats[0]); expect(state.specialPhoneCall).toBe(2); // SPECIALCALL_ROBBED page.once("dialog", (dialog) => dialog.accept()); await page.locator("#save-file").setInputFiles(path); await expect(page.locator("#status")).toContainText("Backup restored"); await page.reload(); await page.locator("#rom-file").setInputFiles(romPath); await expect(page.locator("#status")).toContainText("saved session"); const restored = await backup(page); expect(restored.state).toEqual(state); }); test("the real Cherrygrove return scene battles the rival and commits its exit", async ({ page }) => { await start(page, "questRivalFixtureWorld"); await drive(page); // a pending outdoor call may run before the encounter await step(page, "Right"); const shown = await drive(page); expect(shown).toContain("CHIKORITA"); expect(shown).toContain("???"); await expect(page.locator("#battle")).toBeHidden(); const { state } = await backup(page); expect(state.scenes["26:3"]).toBe(0); expect(state.pokedex.seen).toContain(152); expect(state.party[0].hp).toBe(state.party[0].stats[0]); expect(state.events).toContain(30); }); test("the officer in Elm's lab names the rival through the naming field", async ({ page }) => { await start(page, "questLabFixtureWorld"); await step(page, "Up"); await expect(page.locator("#game-screen")).toHaveAttribute("data-map", "24:5"); await drive(page); for (let y = 11; y > 5; y--) await step(page, "Up"); const shown = await drive(page, "BLUE"); expect(shown).toContain("RIVAL'S NAME?"); expect(shown).toContain("BLUE"); const { state } = await backup(page); expect(state.rivalName).toBe("BLUE"); expect(state.scenes["24:5"]).toBe(2); });