Giving MAIL opens the mail entry: two lines of up to sixteen characters, kept with the player's name and ID, the holder's species and the MAIL. The party submenu's MAIL reads the letter or takes it, either to the PC's MAILBOX (ten at most) or into the pack without its message. The player's PC lists the MAILBOX by author to read, put in the pack or attach, and Bill's PC keeps a Pokémon holding MAIL out of the boxes. Saves keep each holder's MAIL and the MAILBOX. The letter shows its message and author natively; the stationery's graphics are not drawn yet. Closes #27 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
637 lines
27 KiB
TypeScript
637 lines
27 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"
|
|
| "pokegear"
|
|
| "mom"
|
|
| "fly"
|
|
| "headbutt"
|
|
| "fishing"
|
|
| "mail" = 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 }]);
|
|
});
|
|
|
|
test("the Pokégear shows its cards and calls Elm", async ({ page }) => {
|
|
await start(page, "pokegear");
|
|
const text = page.locator("#dialogue-text"),
|
|
choices = page.locator("#dialogue-choice"),
|
|
gear = page.locator("#pokegear"),
|
|
card = page.locator("#pokegear-card");
|
|
await page.keyboard.press("Enter");
|
|
await choices.getByRole("option", { name: /^POKéGEAR/ }).click();
|
|
await expect(gear).toBeVisible();
|
|
await expect(gear.locator('[aria-current="true"]')).toHaveText("CLOCK");
|
|
await expect(card).toContainText("TUESDAY");
|
|
await expect(card).toContainText("12:00 PM");
|
|
await expect(text).toHaveText("Press any button to exit.");
|
|
await page.keyboard.press("ArrowRight");
|
|
await expect(card).toContainText("▶ NEW BARK TOWN");
|
|
await expect(card.locator("canvas")).toHaveAttribute(
|
|
"aria-label",
|
|
"Town Map: NEW BARK TOWN, you are here",
|
|
);
|
|
await expect(page.locator("#native-dialogue")).toBeHidden();
|
|
await page.keyboard.press("ArrowUp");
|
|
await expect(card).toContainText("▶ ROUTE 29");
|
|
// The on-screen controls drive the Pokégear too.
|
|
const right = page.locator('[data-game-button="right"]');
|
|
await right.scrollIntoViewIfNeeded();
|
|
const bounds = (await right.boundingBox())!;
|
|
await page.mouse.move(
|
|
bounds.x + bounds.width / 2,
|
|
bounds.y + bounds.height / 2,
|
|
);
|
|
await page.mouse.down();
|
|
await page.mouse.up();
|
|
await page.locator("#game-screen").focus();
|
|
await expect(card.getByRole("list", { name: "Phone numbers" })).toContainText(
|
|
"PROF.ELM:",
|
|
);
|
|
await expect(text).toHaveText(/Whom do you want\s+to call\?/);
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("KeyX");
|
|
await expect(card).toContainText("CALL");
|
|
await expect(card).toContainText("CANCEL");
|
|
await page.keyboard.press("KeyX");
|
|
await expect(text).toHaveText("……");
|
|
await page.clock.runFor(1000);
|
|
await expect(text).toHaveText("Hello, CHRIS?");
|
|
await page.keyboard.press("KeyX");
|
|
await page.keyboard.press("KeyX");
|
|
await page.keyboard.press("KeyX");
|
|
await expect(text).toHaveText("Click!");
|
|
await page.clock.runFor(3000);
|
|
await expect(text).toHaveText(/Whom do you want\s+to call\?/);
|
|
await page.keyboard.press("KeyZ");
|
|
await expect(gear).toBeHidden();
|
|
await choices.getByRole("option", { name: /^EXIT/ }).click();
|
|
await page.clock.runFor(500);
|
|
await expect(page.locator("#game-notice")).toHaveText("");
|
|
});
|
|
|
|
test("the trainer card turns to the badges and back", async ({ page }) => {
|
|
await start(page, "pokegear");
|
|
const choices = page.locator("#dialogue-choice"),
|
|
card = page.locator("#trainer-card"),
|
|
info = page.locator("#trainer-card-info"),
|
|
body = page.locator("#trainer-card-page");
|
|
await page.keyboard.press("Enter");
|
|
await choices.getByRole("option", { name: /^CHRIS/ }).click();
|
|
await expect(card).toBeVisible();
|
|
await expect(page.locator("#native-dialogue")).toBeHidden();
|
|
await expect(info).toContainText("NAME/");
|
|
await expect(info).toContainText("CHRIS");
|
|
await expect(info).toContainText("ID NO");
|
|
await expect(info).toContainText("MONEY");
|
|
await expect(page.locator("#trainer-card-pic")).toHaveAttribute(
|
|
"aria-label",
|
|
"Chris",
|
|
);
|
|
await expect(body).toContainText("PLAY TIME");
|
|
await expect(body).toContainText("BADGES▶");
|
|
await page.keyboard.press("ArrowRight");
|
|
await expect(body.locator("canvas")).toHaveAttribute(
|
|
"aria-label",
|
|
"0 of 8 Johto badges",
|
|
);
|
|
await page.keyboard.press("ArrowLeft");
|
|
await expect(body).toContainText("PLAY TIME");
|
|
await page.keyboard.press("KeyZ");
|
|
await expect(card).toBeHidden();
|
|
await choices.getByRole("option", { name: /^EXIT/ }).click();
|
|
await page.clock.runFor(500);
|
|
await expect(page.locator("#game-notice")).toHaveText("");
|
|
});
|
|
|
|
test("the options change the battle style and the menu account", async ({
|
|
page,
|
|
}) => {
|
|
await start(page, "pokegear");
|
|
const choices = page.locator("#dialogue-choice"),
|
|
screen = page.locator("#options"),
|
|
list = page.locator("#options-list");
|
|
await page.keyboard.press("Enter");
|
|
await expect(choices).toContainText("Party Pokémon status");
|
|
await choices.getByRole("option", { name: /^OPTION/ }).click();
|
|
await expect(screen).toBeVisible();
|
|
await expect(page.locator("#native-dialogue")).toBeHidden();
|
|
await expect(list.locator('[aria-current="true"]')).toContainText(
|
|
"TEXT SPEED",
|
|
);
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("ArrowRight");
|
|
await expect(list.locator('[aria-current="true"]')).toHaveText(
|
|
"BATTLE STYLESET",
|
|
);
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("ArrowLeft");
|
|
await expect(list.locator('[aria-current="true"]')).toHaveText(
|
|
"MENU ACCOUNTOFF",
|
|
);
|
|
await page.keyboard.press("KeyZ");
|
|
await expect(screen).toBeHidden();
|
|
await expect(choices).not.toContainText("Party Pokémon status");
|
|
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.options).toEqual({
|
|
options: 3 | (1 << 6),
|
|
textboxFrame: 0,
|
|
printerBrightness: 0x40,
|
|
options2: 0,
|
|
});
|
|
});
|
|
|
|
test("Fly picks a visited town on the Fly map and lands there", async ({
|
|
page,
|
|
}) => {
|
|
await start(page, "fly");
|
|
const choices = page.locator("#dialogue-choice"),
|
|
flyMap = page.locator("#fly-map"),
|
|
bubble = page.locator("#fly-map-bubble");
|
|
await page.keyboard.press("Enter");
|
|
await choices.getByRole("option", { name: /^POKéMON/ }).click();
|
|
await choices.getByRole("option", { name: /^PIDGEOT/ }).click();
|
|
await choices.getByRole("option", { name: "FLY", exact: true }).click();
|
|
await expect(flyMap).toBeVisible();
|
|
await expect(page.locator("#native-dialogue")).toBeHidden();
|
|
await expect(bubble).toContainText("Where?");
|
|
await expect(bubble).toContainText("NEW BARK TOWN");
|
|
await expect(page.locator("#fly-map-canvas")).toHaveAttribute(
|
|
"aria-label",
|
|
"Fly map: NEW BARK TOWN, you are here",
|
|
);
|
|
// Up skips to the next visited flypoint; B returns to the party menu.
|
|
await page.keyboard.press("ArrowUp");
|
|
await expect(bubble).toContainText("CHERRYGROVE CITY");
|
|
await page.keyboard.press("KeyZ");
|
|
await expect(flyMap).toBeHidden();
|
|
await expect(page.locator("#dialogue-text")).toHaveText("Choose a POKéMON.");
|
|
await choices.getByRole("option", { name: /^PIDGEOT/ }).click();
|
|
await choices.getByRole("option", { name: "FLY", exact: true }).click();
|
|
await page.keyboard.press("ArrowUp");
|
|
await expect(bubble).toContainText("CHERRYGROVE CITY");
|
|
await page.keyboard.press("KeyX");
|
|
await expect(flyMap).toBeHidden();
|
|
await page.clock.runFor(5_000);
|
|
const screen = page.locator("#game-screen");
|
|
await expect(screen).toHaveAttribute("data-map", "26:3");
|
|
await expect(screen).toHaveAttribute("data-x", "29");
|
|
await expect(screen).toHaveAttribute("data-y", "4");
|
|
await expect(screen).toHaveAttribute("data-activity", "idle");
|
|
await expect(page.locator("#game-notice")).toHaveText("");
|
|
const { state } = await backup(page);
|
|
expect([state.map, state.x, state.y]).toEqual(["26:3", 29, 4]);
|
|
});
|
|
|
|
test("Headbutt shakes a tree and a sleeping Hoothoot falls out", async ({
|
|
page,
|
|
}) => {
|
|
await start(page, "headbutt");
|
|
const text = page.locator("#dialogue-text"),
|
|
choices = page.locator("#dialogue-choice"),
|
|
screen = page.locator("#game-screen");
|
|
await page.keyboard.press("KeyX");
|
|
await expect(text).toHaveText(/A POKéMON could be\s+in this tree\./);
|
|
await page.keyboard.press("KeyX");
|
|
await expect(text).toHaveText(/Want to HEADBUTT\s+it\?/);
|
|
await choices.getByRole("button", { name: "YES" }).click();
|
|
await expect(text).toHaveText(/TOTODILE did a\s+HEADBUTT!/);
|
|
await page.keyboard.press("KeyX");
|
|
await expect(page.locator("#native-dialogue")).toBeHidden();
|
|
await page.clock.runFor(5_000);
|
|
await expect(page.locator("#battle")).toBeVisible();
|
|
await expect(page.locator("#battle-text")).toHaveText(
|
|
/HOOTHOOT fell\s+out of the tree!/,
|
|
);
|
|
await expect(page.locator("#battle-foe-name")).toHaveText("HOOTHOOT");
|
|
await expect(page.locator("#battle-foe-status")).toHaveText("SLP");
|
|
await expect(page.locator("#game-notice")).toHaveText("");
|
|
await expect(screen).toHaveAttribute("data-map", "24:3");
|
|
});
|
|
|
|
test("Select moves an item within the pack", async ({ page }) => {
|
|
await start(page, "candy");
|
|
const choices = page.locator("#dialogue-choice"),
|
|
pack = page.locator("#mart");
|
|
await page.keyboard.press("Enter");
|
|
await choices.getByRole("option", { name: /^PACK/ }).click();
|
|
const entries = pack.locator("#mart-menu button");
|
|
await expect(entries.first()).toContainText("POTION");
|
|
// Select on the highlighted Potions, down to the Rare Candy, Select.
|
|
await page.keyboard.press("ShiftLeft");
|
|
await expect(page.locator("#mart-prompt")).toHaveText(
|
|
"Where should this be moved to?",
|
|
);
|
|
await expect(entries.first()).toContainText("▷POTION");
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.press("ShiftLeft");
|
|
await expect(page.locator("#mart-prompt")).toHaveText("");
|
|
await expect(entries.nth(0)).toContainText("RARE CANDY");
|
|
await expect(entries.nth(1)).toContainText("POTION");
|
|
// B leaves the pack, then the start menu, which commits the change.
|
|
await page.keyboard.press("KeyZ");
|
|
await expect(pack).toBeHidden();
|
|
await page.keyboard.press("KeyZ");
|
|
await page.clock.runFor(500);
|
|
await expect(page.locator("#native-dialogue")).toBeHidden();
|
|
const { state } = await backup(page);
|
|
expect(state.bag.items.map((slot: { item: number }) => slot.item)).toEqual([
|
|
0x20, 18,
|
|
]);
|
|
});
|
|
|
|
test("MAIL is written when given, and read from the party menu", async ({
|
|
page,
|
|
}) => {
|
|
await start(page, "mail");
|
|
const text = page.locator("#dialogue-text"),
|
|
choices = page.locator("#dialogue-choice"),
|
|
pack = page.locator("#mart"),
|
|
letter = page.locator("#mail-view");
|
|
await page.keyboard.press("Enter");
|
|
await choices.getByRole("option", { name: /^PACK/ }).click();
|
|
await pack.getByRole("button", { name: /^FLOWER MAIL/ }).click();
|
|
await choices.getByRole("option", { name: "GIVE", exact: true }).click();
|
|
await choices.getByRole("option", { name: /^CYNDAQUIL/ }).click();
|
|
await expect(text).toHaveText(/Made CYNDAQUIL\s+hold FLOWER MAIL\./);
|
|
await page.keyboard.press("KeyX");
|
|
await choices.getByLabel("MAIL line 1").fill("HELLO, CHRIS!");
|
|
await choices.getByLabel("MAIL line 2").fill("SEE YOU!");
|
|
await choices.getByRole("button", { name: "OK" }).click();
|
|
await expect(pack).toBeVisible();
|
|
await page.keyboard.press("KeyZ");
|
|
await choices.getByRole("option", { name: /^POKéMON/ }).click();
|
|
await choices.getByRole("option", { name: /^CYNDAQUIL/ }).click();
|
|
await choices.getByRole("option", { name: "MAIL", exact: true }).click();
|
|
await choices.getByRole("option", { name: "READ", exact: true }).click();
|
|
await expect(letter).toBeVisible();
|
|
await expect(page.locator("#mail-type")).toHaveText("FLOWER MAIL");
|
|
await expect(page.locator("#mail-message")).toHaveText(
|
|
"HELLO, CHRIS! \nSEE YOU!",
|
|
);
|
|
await expect(page.locator("#mail-author")).toHaveText("CHRIS");
|
|
await page.locator("#mail-close").click();
|
|
await expect(letter).toBeHidden();
|
|
await expect(page.locator("#game-notice")).toHaveText("");
|
|
});
|
|
|
|
test("the Old Rod hooks a Magikarp from New Bark Town's shore", async ({
|
|
page,
|
|
}) => {
|
|
await start(page, "fishing");
|
|
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 page.keyboard.press("ArrowRight");
|
|
await page.keyboard.press("ArrowRight");
|
|
await pack.getByRole("button", { name: "OLD ROD", exact: true }).click();
|
|
await choices.getByRole("option", { name: "USE", exact: true }).click();
|
|
// The cast, 40 frames, four bites with "!", 40 more frames.
|
|
await page.clock.runFor(5_000);
|
|
await expect(text).toHaveText(/Oh!\s+A bite!/);
|
|
await page.keyboard.press("KeyX");
|
|
await page.clock.runFor(5_000);
|
|
await expect(page.locator("#battle")).toBeVisible();
|
|
await expect(page.locator("#battle-text")).toHaveText(
|
|
/The hooked\s+MAGIKARP\s+attacked!/,
|
|
);
|
|
await expect(page.locator("#battle-foe-name")).toHaveText("MAGIKARP");
|
|
await expect(page.locator("#game-notice")).toHaveText("");
|
|
});
|
|
|
|
test("Mom keeps the player's money with the digit box", async ({ page }) => {
|
|
await start(page, "mom");
|
|
const text = page.locator("#dialogue-text"),
|
|
choices = page.locator("#dialogue-choice"),
|
|
box = choices.locator(".mom-bank");
|
|
await page.keyboard.press("KeyX");
|
|
await expect(text).toHaveText(/Hi! Welcome home!/);
|
|
await page.keyboard.press("KeyX");
|
|
await page.keyboard.press("KeyX");
|
|
await expect(text).toHaveText(/Or is this about\s+your money\?/);
|
|
await choices.getByRole("button", { name: "YES" }).click();
|
|
await expect(text).toHaveText(/What do you want\s+to do\?/);
|
|
await choices.getByRole("option", { name: "SAVE" }).click();
|
|
await expect(text).toHaveText(/How much do you\s+want to save\?/);
|
|
await expect(box).toContainText("SAVED¥2300");
|
|
await expect(box).toContainText("HELD¥3000");
|
|
await expect(box).toContainText("DEPOSIT¥000000");
|
|
await expect(box.locator(".mom-bank-cursor")).toHaveCount(1);
|
|
// The keyboard moves to the thousands and raises it; the on-screen
|
|
// buttons raise it again.
|
|
for (let i = 0; i < 3; i++) await page.keyboard.press("ArrowLeft");
|
|
await page.keyboard.press("ArrowUp");
|
|
await expect(box).toContainText("DEPOSIT¥001000");
|
|
await choices.getByRole("button", { name: "Increase digit" }).click();
|
|
await expect(box).toContainText("DEPOSIT¥002000");
|
|
await page.locator("#game-screen").focus();
|
|
await page.keyboard.press("KeyX");
|
|
await expect(text).toHaveText(/Your money's safe\s+here! Get going!/);
|
|
await page.keyboard.press("KeyX");
|
|
await expect(page.locator("#native-dialogue")).toBeHidden();
|
|
const { state } = await backup(page);
|
|
expect([state.money, state.momsMoney]).toEqual([1000, 4300]);
|
|
});
|