Files
Crystal/tests/foundation-browser.spec.ts
T
Sébastien QUEROLandClaude Opus 5.5 3456aa4e11
Checks / web (push) Canceled after 0s
Port field moves from the menu, SEL with Select, and moving Pokémon in Bill's PC
Field moves chosen in the party menu run their original checks and the
ROM's from-menu scripts (Cut, Surf, Strength, Flash, Whirlpool, Waterfall,
Sweet Scent), and Softboiled and Milk Drink share HP. SEL registers a key
item that Select then uses, replacing the Key Items shortcut, and Bill's
PC moves Pokémon between the party and every box.

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

298 lines
13 KiB
TypeScript

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" | "candy" = 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("SEL registers the Bicycle, and Select rides it with keyboard and touch", async ({
page,
}) => {
await start(page);
const text = page.locator("#dialogue-text"),
choices = page.locator("#dialogue-choice"),
pack = page.locator("#mart");
await page.keyboard.press("Enter");
await choices.getByRole("option", { name: /^PACK/ }).click();
// Right twice: the Balls pocket, then Key Items.
await page.keyboard.press("ArrowRight");
await page.keyboard.press("ArrowRight");
await pack.getByRole("button", { name: "BICYCLE", exact: true }).click();
await choices.getByRole("option", { name: "SEL", exact: true }).click();
await expect(text).toContainText("Registered the");
await page.keyboard.press("KeyX");
await page.keyboard.press("KeyZ");
await page.keyboard.press("KeyZ");
await expect(choices).toBeHidden();
await page.keyboard.press("ShiftLeft");
await page.clock.runFor(500);
await expect(page.locator("#game-notice")).toHaveText("");
expect((await backup(page)).state.playerState).toBe(1);
await page.locator("#tab-game").click();
await page.locator("#play").click();
await page.getByRole("button", { name: "select", exact: true }).click();
await page.clock.runFor(500);
expect((await backup(page)).state.playerState).toBe(0);
});
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("Select without a registered item explains SEL, and the Bicycle stays packed indoors", async ({
page,
}) => {
await start(page, true);
const text = page.locator("#dialogue-text"),
choices = page.locator("#dialogue-choice"),
pack = page.locator("#mart");
await page.keyboard.press("ShiftLeft");
await expect(text).toContainText("An item in your");
await page.keyboard.press("KeyX");
await page.keyboard.press("KeyX");
await page.keyboard.press("Enter");
await choices.getByRole("option", { name: /^PACK/ }).click();
await page.keyboard.press("ArrowLeft");
await page.keyboard.press("ArrowLeft");
await pack.getByRole("button", { name: "BICYCLE", exact: true }).click();
await choices.getByRole("option", { name: "USE", exact: true }).click();
await expect(text).toContainText("This isn't the");
await page.keyboard.press("KeyX");
await pack.getByRole("button", { name: "BACK", exact: true }).click();
await page.keyboard.press("KeyZ");
await page.clock.runFor(500);
await expect(page.locator("#game-notice")).toHaveText("");
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 }]);
});
test("a Rare Candy levels up and evolves a Pokémon through the pack", async ({
page,
}) => {
await start(page, "candy");
const text = page.locator("#dialogue-text"),
choices = page.locator("#dialogue-choice"),
pack = page.locator("#mart");
await page.keyboard.press("Enter");
await choices.getByRole("option", { name: /^PACK/ }).click();
await pack.getByRole("button", { name: /RARE CANDY/ }).click();
await choices.getByRole("option", { name: "USE", exact: true }).click();
await choices.getByRole("option", { name: /^CYNDAQUIL/ }).click();
await expect(text).toContainText("grew to");
await page.keyboard.press("KeyX");
await expect(text).toContainText("ATTACK");
await page.keyboard.press("KeyX");
await expect(text).toContainText("is evolving!");
await page.clock.runFor(1500);
await expect(page.locator("#pokepic")).toBeVisible();
await page.clock.runFor(8000);
await expect(text).toContainText("Congratulations!");
await page.keyboard.press("KeyX");
await expect(text).toContainText("QUILAVA!");
await page.keyboard.press("KeyX");
await expect(pack).toHaveAttribute("aria-label", "Pack");
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[0].species, state.party[0].level]).toEqual([156, 14]);
expect(state.bag.items).toEqual([{ item: 18, quantity: 5 }]);
});