Files
Crystal/tests/foundation-browser.spec.ts
T
Sébastien QUEROLandClaude Opus 5.5 1af2beacdc
Checks / web (push) Canceled after 0s
Port the start menu, the party menu and the pack outside battle
Start opens the original start menu. POKéMON shows the party with the
Pokémon submenu (stats, switching, move order, held items); PACK shows the
four pockets with the field item effects for healing, PP and the Bicycle,
plus giving and tossing. The menu runs on a script draft, and its cursors
persist until a battle clears them. Select keeps the Key Items shortcut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:10:22 +02:00

242 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, room: boolean | "poster" | "center" = false) {
await page.route("**/src/game/runtime.ts", async (route) => {
const response = await route.fetch(),
source = await response.text();
const needle = "const world = new GameWorld(assets);";
expect(source).toContain(needle);
// Fixture worlds stand in for a saved session, so no introduction runs.
expect(source).toContain("this.world?.beginIntro();");
await route.fulfill({
response,
body: source.replace("this.world?.beginIntro();", "").replace(
needle,
`const { foundationFixture } = await import('/tests/foundation-fixture.ts'); const world = foundationFixture(assets, ${JSON.stringify(room)});`,
),
});
});
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 backup(page: Page) {
await page.locator("#tab-saves").click();
const download = page.waitForEvent("download");
await page.locator("#backup").click();
const path = (await (await download).path())!;
const record = JSON.parse(readFileSync(path, "utf8"));
return {
path,
state: JSON.parse(Buffer.from(record.state, "base64").toString("utf8")),
};
}
test("Select opens Key Items, freezes movement, and uses the Bicycle with keyboard and touch", async ({
page,
}) => {
await start(page);
await page.keyboard.press("ShiftLeft");
await expect(page.locator("#field-menu")).toBeVisible();
await page.keyboard.down("ArrowDown");
await page.clock.runFor(1000);
await page.keyboard.up("ArrowDown");
await expect(page.locator("#game-screen")).toHaveAttribute("data-y", "6");
await page
.locator("#field-items")
.getByRole("button", { name: "BICYCLE", exact: true })
.click();
await page.keyboard.press("KeyX");
await page.clock.runFor(500);
await expect(page.locator("#field-menu")).toBeHidden();
await expect(page.locator("#game-notice")).toHaveText("");
expect((await backup(page)).state.playerState).toBe(1);
});
test("the bedroom PC puts away a decoration and saves the result", async ({
page,
}) => {
await start(page, true);
await page.keyboard.press("KeyX");
await expect(page.locator("#game-notice")).toHaveText("");
await expect(page.locator("#dialogue-text")).toContainText(
"turned on the PC.",
);
await page.keyboard.press("KeyX");
const choices = page.locator("#dialogue-choice");
await choices
.getByRole("option", { name: "DECORATION", exact: true })
.click();
await expect(page.locator("#dialogue-text")).toHaveText("DECORATION");
await choices.getByRole("option", { name: "BED", exact: true }).click();
await choices
.getByRole("option", { name: "PUT IT AWAY", exact: true })
.click();
await page.keyboard.press("KeyX");
await choices.getByRole("option", { name: "EXIT", exact: true }).click();
await page.clock.runFor(500);
await expect(page.locator("#game-notice")).toHaveText("");
expect((await backup(page)).state.decorations.bed).toBe(0);
});
test("B cancels the pack and an indoor Bicycle attempt leaves the player walking", async ({
page,
}) => {
await start(page, true);
await page.keyboard.press("ShiftLeft");
await page.keyboard.press("KeyZ");
await expect(page.locator("#field-menu")).toBeHidden();
await page.keyboard.press("ShiftLeft");
await page
.locator("#field-items")
.getByRole("button", { name: "BICYCLE", exact: true })
.click();
await expect(page.locator("#game-notice")).toContainText("can’t use");
expect((await backup(page)).state.playerState).toBe(0);
});
test("the Town Map poster opens a native map and closes with B", async ({
page,
}) => {
await page.setViewportSize({ width: 320, height: 568 });
await start(page, "poster");
await page.keyboard.press("KeyX");
await expect(page.locator("#dialogue-text")).toContainText("TOWN MAP");
await page.keyboard.press("KeyX");
await expect(page.locator("#town-map-view")).toBeVisible();
const colors = await page
.locator("#town-map-canvas")
.evaluate(
(canvas: HTMLCanvasElement) =>
new Set(canvas.getContext("2d")!.getImageData(0, 0, 160, 144).data)
.size,
);
expect(colors).toBeGreaterThan(8);
await page.keyboard.press("KeyZ");
await expect(page.locator("#town-map-view")).toBeHidden();
await expect(page.locator("#game-notice")).toHaveText("");
});
test("the Pokémon Center PC deposits a Pokémon and an item", async ({
page,
}) => {
await start(page, "center");
const text = page.locator("#dialogue-text"),
choices = page.locator("#dialogue-choice");
await page.keyboard.press("KeyX");
await expect(text).toContainText("turned on the PC.");
await page.keyboard.press("KeyX");
await choices.getByRole("option", { name: "BILL's PC", exact: true }).click();
await expect(text).toContainText("BILL's PC");
await page.keyboard.press("KeyX");
await page.keyboard.press("KeyX");
await choices
.getByRole("option", { name: "DEPOSIT POKéMON", exact: true })
.click();
await choices.getByRole("option", { name: "PIDGEY :L4" }).click();
await choices.getByRole("option", { name: "DEPOSIT", exact: true }).click();
await expect(text).toHaveText("Stored PIDGEY!");
await page.clock.runFor(2000);
await expect(choices.getByRole("option", { name: "PIDGEY :L4" })).toHaveCount(
0,
);
// B leaves the party list, then Bill's PC.
await page.keyboard.press("KeyZ");
await page.keyboard.press("KeyZ");
await expect(choices.getByRole("option", { name: "TURN OFF" })).toBeVisible();
await choices
.getByRole("option", { name: /'s PC$/ })
.last()
.click();
await page.keyboard.press("KeyX");
await page.keyboard.press("KeyX");
await choices
.getByRole("option", { name: "DEPOSIT ITEM", exact: true })
.click();
const storage = page.locator("#mart");
await expect(storage).toHaveAttribute("aria-label", "Item storage");
await expect(page.locator("#mart-money")).toBeHidden();
await storage.getByRole("button", { name: /POTION/ }).click();
await storage.getByRole("button", { name: "Plus 1", exact: true }).click();
await storage.getByRole("button", { name: "DEPOSIT", exact: true }).click();
await expect(text).toContainText("Deposited 2 POTION(S).");
await page.keyboard.press("KeyX");
await storage.getByRole("button", { name: "BACK", exact: true }).click();
await choices.getByRole("option", { name: "LOG OFF", exact: true }).click();
await choices.getByRole("option", { name: "TURN OFF", exact: true }).click();
await page.keyboard.press("KeyX");
await page.clock.runFor(500);
await expect(page.locator("#game-notice")).toHaveText("");
const { state } = await backup(page);
expect(state.party).toHaveLength(1);
expect(
state.storage.boxes[0].map((mon: { species: number }) => mon.species),
).toEqual([16]);
expect(state.pcItems).toEqual([{ item: 18, quantity: 2 }]);
expect(state.bag.items).toEqual([{ item: 18, quantity: 3 }]);
});
test("Start opens the start menu: SWITCH reorders the party and the pack tosses an item", async ({
page,
}) => {
await start(page, "center");
const text = page.locator("#dialogue-text"),
choices = page.locator("#dialogue-choice"),
pack = page.locator("#mart");
await page.keyboard.press("Enter");
await expect(choices.getByRole("option", { name: /^POKéMON/ })).toBeVisible();
await expect(choices.getByRole("option", { name: /^PACK/ })).toBeVisible();
// Movement is frozen while the menu is open.
await page.keyboard.down("ArrowLeft");
await page.clock.runFor(500);
await page.keyboard.up("ArrowLeft");
await expect(page.locator("#game-screen")).toHaveAttribute("data-x", "9");
await choices.getByRole("option", { name: /^POKéMON/ }).click();
await expect(text).toHaveText("Choose a POKéMON.");
await choices.getByRole("option", { name: /^CYNDAQUIL/ }).click();
await choices.getByRole("option", { name: "SWITCH", exact: true }).click();
await expect(text).toHaveText("Move to where?");
await choices.getByRole("option", { name: /^PIDGEY/ }).click();
await expect(choices.getByRole("option").first()).toHaveAccessibleName(
/^PIDGEY/,
);
// B leaves the party menu for the start menu.
await page.keyboard.press("KeyZ");
await choices.getByRole("option", { name: /^PACK/ }).click();
await expect(pack).toHaveAttribute("aria-label", "Pack");
await expect(
pack.getByRole("button", { name: "ITEM POCKET", exact: true }),
).toHaveAttribute("aria-pressed", "true");
// Right twice: the Balls pocket, then Key Items.
await page.keyboard.press("ArrowRight");
await page.keyboard.press("ArrowRight");
await expect(pack.getByRole("button", { name: "BICYCLE" })).toBeVisible();
await pack.getByRole("button", { name: "ITEM POCKET", exact: true }).click();
await pack.getByRole("button", { name: /POTION/ }).click();
await choices.getByRole("option", { name: "TOSS", exact: true }).click();
await expect(page.locator("#mart-prompt")).toContainText("Throw away how");
await pack.getByRole("button", { name: "TOSS", exact: true }).click();
await expect(text).toContainText("Throw away 1 POTION(S)?");
await choices.getByRole("button", { name: /YES/ }).click();
await expect(text).toContainText("Threw away");
await page.keyboard.press("KeyX");
await pack.getByRole("button", { name: "BACK", exact: true }).click();
await choices.getByRole("option", { name: /^EXIT/ }).click();
await page.clock.runFor(500);
await expect(page.locator("#game-notice")).toHaveText("");
const { state } = await backup(page);
expect(state.party.map((mon: { species: number }) => mon.species)).toEqual([
16, 155,
]);
expect(state.bag.items).toEqual([{ item: 18, quantity: 4 }]);
});