import { expect, test } from "bun:test"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; process.env.FORGE_APP_DATA = mkdtempSync(join(tmpdir(), "lanternfall-web-")); process.env.PORT = "0"; const { server } = await import("./server.ts"); const base = `http://${server.hostname}:${server.port}`; class Player { cookie = ""; private keep(response: Response): void { const set = response.headers.get("set-cookie"); if (set) this.cookie = set.split(";")[0] ?? ""; } async look(path = "/"): Promise { const response = await fetch(base + path, { headers: this.cookie === "" ? {} : { cookie: this.cookie }, }); this.keep(response); return response.text(); } async act(action: string): Promise { const response = await fetch(`${base}/act`, { method: "POST", body: new URLSearchParams({ action }), redirect: "manual", headers: { "content-type": "application/x-www-form-urlencoded", ...(this.cookie === "" ? {} : { cookie: this.cookie }), }, }); this.keep(response); return response; } } test("the dungeon opens with a delver, a map and a lantern", async () => { const page = await new Player().look(); expect(page).toContain("lanternfall"); expect(page).toContain("health"); expect(page).toContain("12 / 12"); expect(page).toContain('aria-label="the dungeon floor, drawn in ascii"'); // 48 x 24 cells, all rendered. expect((page.match(/ { const response = await fetch(`${base}/`); const set = response.headers.get("set-cookie") ?? ""; expect(set).toMatch(/delver=[a-f0-9]{32}/); expect(set).toContain("HttpOnly"); expect(set).toContain("SameSite=Lax"); }); test("moving takes a turn and is remembered", async () => { const player = new Player(); await player.look(); const moved = await player.act("west"); expect(moved.status).toBe(303); expect(moved.headers.get("location")).toBe("/"); const page = await player.look(); expect(page).toMatch(/turns<\/span>[1-9]/); }); test("an action the game does not have is refused, not guessed at", async () => { const player = new Player(); await player.look(); const response = await player.act("teleport"); expect(response.status).toBe(303); expect(response.headers.get("location")).toBe("/"); expect(await player.look()).toContain("12 / 12"); }); test("two players share one dungeon and one map", async () => { const first = new Player(); await first.look(); for (const move of ["west", "west", "west", "north"]) await first.act(move); const second = new Player(); const page = await second.look(); // The tiles the first one lit are remembered on the second one's screen. expect((page.match(/class="m"/g) ?? []).length).toBeGreaterThan(0); expect(page).toContain("tiles lit, ever"); }); test("the map hides what has never been lit", async () => { const page = await new Player().look(); expect((page.match(/class="u"/g) ?? []).length).toBeGreaterThan(400); }); test("the keyboard layer is a file, not an inline script", async () => { const page = await new Player().look(); expect(page).toContain(''); expect(page).not.toMatch(/