Something went wrong. Try again.
geometric packing game (tetris-ish) on an icosahedron
Something went wrong. Try again.
21 kB · 471 lines
TypeScript
at appview
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472import { expect, test, type Page } from "@playwright/test";
type MigrationProbe = { documentToken: string; frames: Map<number, true>; resizeObservers: Set<ResizeObserver>; webglContexts: Array<{ canvas: HTMLCanvasElement; context: WebGLRenderingContext | WebGL2RenderingContext; }>; audioContexts: AudioContext[]; canvasBeforeSave?: HTMLCanvasElement; canvasBeforeResume?: HTMLCanvasElement; globalSignalListeners: Set<{ target: EventTarget; type: string; listener: EventListener | EventListenerObject; capture: boolean; }>;};
declare global { interface Window { __migrationProbe?: MigrationProbe; }}
const browserProbeScript = `(() => { const probe = { documentToken: String(Date.now()) + "-" + Math.random(), frames: new Map(), resizeObservers: new Set(), webglContexts: [], audioContexts: [], globalSignalListeners: new Set(), }; Object.defineProperty(window, "__migrationProbe", { value: probe });
const requestFrame = window.requestAnimationFrame.bind(window); const cancelFrame = window.cancelAnimationFrame.bind(window); window.requestAnimationFrame = (callback) => { let id; id = requestFrame((time) => { probe.frames.delete(id); callback(time); }); probe.frames.set(id, true); return id; }; window.cancelAnimationFrame = (id) => { probe.frames.delete(id); cancelFrame(id); };
const NativeResizeObserver = window.ResizeObserver; Object.defineProperty(window, "ResizeObserver", { configurable: true, writable: true, value: class extends NativeResizeObserver { constructor(callback) { super(callback); probe.resizeObservers.add(this); } disconnect() { probe.resizeObservers.delete(this); super.disconnect(); } }, });
const getContext = HTMLCanvasElement.prototype.getContext; HTMLCanvasElement.prototype.getContext = function(type, ...args) { const context = getContext.call(this, type, ...args); if (context && /^(webgl|webgl2|experimental-webgl)$/.test(String(type))) { probe.webglContexts.push({ canvas: this, context }); } return context; };
if (window.AudioContext) { const NativeAudioContext = window.AudioContext; Object.defineProperty(window, "AudioContext", { configurable: true, writable: true, value: new Proxy(NativeAudioContext, { construct(target, args, newTarget) { const context = Reflect.construct(target, args, newTarget); probe.audioContexts.push(context); return context; }, }), }); }
const nativeAdd = EventTarget.prototype.addEventListener; const nativeRemove = EventTarget.prototype.removeEventListener; const isGlobalTarget = (target) => target === window || target === document; const captureFor = (options) => typeof options === "boolean" ? options : Boolean(options && options.capture); const forget = (target, type, listener, capture) => { for (const entry of probe.globalSignalListeners) { if (entry.target === target && entry.type === type && entry.listener === listener && entry.capture === capture) { probe.globalSignalListeners.delete(entry); } } };
EventTarget.prototype.addEventListener = function(type, listener, options) { nativeAdd.call(this, type, listener, options); const signal = options && typeof options === "object" ? options.signal : null; if (isGlobalTarget(this) && listener && signal && !signal.aborted) { const capture = captureFor(options); const duplicate = Array.from(probe.globalSignalListeners).some((entry) => entry.target === this && entry.type === type && entry.listener === listener && entry.capture === capture, ); if (!duplicate) { const entry = { target: this, type, listener, capture }; probe.globalSignalListeners.add(entry); signal.addEventListener("abort", () => probe.globalSignalListeners.delete(entry), { once: true }); } } };
EventTarget.prototype.removeEventListener = function(type, listener, options) { nativeRemove.call(this, type, listener, options); if (isGlobalTarget(this) && listener) forget(this, type, listener, captureFor(options)); };})();`;
async function installBrowserProbe(page: Page) { await page.addInitScript(browserProbeScript);}
async function probe(page: Page) { return page.evaluate(() => { const current = window.__migrationProbe!; const activeContexts = current.webglContexts.filter(({ context }) => !context.isContextLost());
return { documentToken: current.documentToken, frames: current.frames.size, resizeObservers: current.resizeObservers.size, globalSignalListeners: current.globalSignalListeners.size, activeWebGLContexts: activeContexts.length, connectedWebGLContexts: activeContexts.filter(({ canvas }) => canvas.isConnected).length, sceneCanvases: document.querySelectorAll("#scene canvas.three-canvas").length, audioStates: current.audioContexts.map((context) => context.state), }; });}
async function expectOneMountedGame(page: Page) { await expect(page.locator("#scene canvas.three-canvas")).toHaveCount(1); await page.waitForFunction(() => { const current = window.__migrationProbe!;
return ( document.querySelectorAll("#scene canvas.three-canvas").length === 1 && current.frames.size > 0 && current.resizeObservers.size === 1 && current.webglContexts.filter(({ canvas, context }) => canvas.isConnected && !context.isContextLost(), ).length === 1 ); });
const current = await probe(page); expect(current.sceneCanvases).toBe(1); expect(current.activeWebGLContexts).toBe(1); expect(current.connectedWebGLContexts).toBe(1); expect(current.resizeObservers).toBe(1); expect(current.frames).toBeGreaterThan(0);}
test("canonical routes server-render headings and metadata", async ({ request, page }) => { const published = "/scenarios/published/?uri=at%3A%2F%2Fdid%3Aplc%3Atest%2Fnet.oscillatory.radcohesion.scenario%2Fexample&cid=baf";
const routes = [ ["/game/", "Rad Cohesion", null], ["/leaderboards/", "Leaderboards · Rad Cohesion", /<h1[^>]*>Leaderboards<\/h1>/], ["/library/?tab=saved", "Library · Rad Cohesion", /<h1[^>]*>Library<\/h1>/], ["/library/?tab=runs", "Library · Rad Cohesion", /<h1[^>]*>Library<\/h1>/], ["/library/?tab=scenarios", "Library · Rad Cohesion", /<h1[^>]*>Library<\/h1>/], ["/scenarios/", "Scenarios · Rad Cohesion", /<h1[^>]*>Scenarios<\/h1>/], [published, "Published scenario · Rad Cohesion", /<h1[^>]*>Published scenario<\/h1>/], ] as const;
for (const [route, title, heading] of routes) { const response = await request.get(route); expect(response.ok(), route).toBeTruthy(); const html = await response.text(); expect(html, route).toContain(`<title>${title}</title>`);
if (heading) expect(html, route).toMatch(heading); expect(html, route).toContain('property="og:title" content="Rad Cohesion"');
if (route === "/game/") { expect(html).toContain('<meta name="description" content="A geometric packing puzzle game played on an icosahedron."'); expect(html).toContain('property="og:image"'); expect(html).toContain('name="twitter:card" content="summary_large_image"'); expect(html).toMatch(/<link rel="manifest" href="[^"]*manifest\.webmanifest"/); }
if (route === "/library/?tab=saved") expect(html).toContain("Saved games");
if (route === "/library/?tab=runs") expect(html).toContain("Finished runs"); }
await installBrowserProbe(page); await page.goto("/?mode=free&seed=1234#keep-this-fragment"); await expectOneMountedGame(page); await expect(page).toHaveURL(/\/game\/#keep-this-fragment$/); await expect(page.locator("#free-play-tools")).toBeVisible();
await page.goto("/library/?tab=saved"); await expect(page.getByRole("heading", { name: "Library", exact: true })).toBeVisible(); const gamesTabs = page.getByRole("navigation", { name: "Library sections" }); await expect(gamesTabs.getByRole("link", { name: "Saved games" })).toHaveAttribute("aria-current", "page"); await gamesTabs.getByRole("link", { name: "Finished runs" }).click(); await expect(page).toHaveURL(/\/library\/?\?tab=runs$/); await expect(gamesTabs.getByRole("link", { name: "Finished runs" })).toHaveAttribute("aria-current", "page"); await expect(page.locator(".runs-note")).toBeVisible();});
test("mode switching replaces the game history entry, so Back returns to the previous page", async ({ page }) => { await installBrowserProbe(page);
await page.goto("/library/"); await expect(page.locator(".site-account-button")).toBeEnabled(); const documentToken = (await probe(page)).documentToken; await page.getByRole("navigation", { name: "Main navigation" }).getByRole("link", { name: "Play", exact: true }).click(); await expect(page).toHaveURL(/\/game\/$/); await expectOneMountedGame(page); await expect(page.locator("#strikes-stat")).toBeVisible(); await expect(page.locator("#free-play-tools")).toBeHidden(); expect((await probe(page)).documentToken).toBe(documentToken);
await page.locator("#mode-switch").click(); await expect(page).toHaveURL(/\/game\/$/); await expectOneMountedGame(page); await expect(page.locator("#free-play-tools")).toBeVisible(); await expect(page.locator("#strikes-stat")).toBeHidden(); expect((await probe(page)).documentToken).toBe(documentToken);
await page.goBack(); await expect(page).toHaveURL(/\/library\/$/); await expect(page.getByRole("heading", { name: "Library", exact: true })).toBeVisible(); expect((await probe(page)).documentToken).toBe(documentToken); expect((await probe(page)).sceneCanvases).toBe(0);});
test("saved games are named in the game form, listed canonically, and resume as distinct checkpoints", async ({ page }) => { await installBrowserProbe(page); await page.goto("/game/?mode=timed&seed=1234"); await expectOneMountedGame(page); await expect(page).toHaveURL(/\/game\/$/); const documentToken = (await probe(page)).documentToken;
await page.getByRole("button", { name: "Start", exact: true }).click(); await page.locator("#rotate-right").click(); await expect(page.locator("#orientation")).toContainText("R2"); await page.locator("#saved-games-button").click(); await expect(page.locator("#saved-games-dialog")).toBeVisible(); await expect(page.locator("#saved-games-list, #scenarios-list")).toHaveCount(0); await page.evaluate(() => { window.__migrationProbe!.canvasBeforeSave = document.querySelector<HTMLCanvasElement>("#scene canvas.three-canvas") ?? undefined; });
await page.locator("#save-game-name").fill("Timed checkpoint A"); await page.locator("#save-game-submit").click(); await expect(page.locator("#save-games-status")).toContainText("Saved"); await page.locator("#saved-games-close").click(); await page.locator("#rotate-right").click(); await expect(page.locator("#orientation")).toContainText("R3");
await page.locator("#saved-games-button").click(); await page.locator("#save-game-name").fill("Timed checkpoint B"); await page.getByRole("button", { name: "Save as new game", exact: true }).click(); await expect(page.locator("#save-games-status")).toContainText("Created"); await expect(page).toHaveURL(/\/game\/$/);
const saves = await page.evaluate(() => JSON.parse(localStorage.getItem("rad-cohesion:game-saves:v1")!).saves, );
expect(saves).toHaveLength(2); expect(saves[0].id).not.toBe(saves[1].id); expect(saves.find((save: { name: string }) => save.name === "Timed checkpoint A").snapshot.game.rotation).toBe(1); expect(saves.find((save: { name: string }) => save.name === "Timed checkpoint B").snapshot.game.rotation).toBe(2);
const afterSave = await probe(page); expect(afterSave.documentToken).toBe(documentToken); expect(afterSave.sceneCanvases).toBe(1); expect(afterSave.activeWebGLContexts).toBe(1); expect(afterSave.resizeObservers).toBe(1); expect(await page.evaluate(() => window.__migrationProbe!.canvasBeforeSave === document.querySelector("#scene canvas.three-canvas"), )).toBe(true); await page.locator("#saved-games-close").click(); await expect(page.locator("#pause")).toHaveText("Pause");
const mainNavigation = page.getByRole("navigation", { name: "Main navigation" }); await mainNavigation.getByRole("link", { name: "Library", exact: true }).click(); await expect(page).toHaveURL(/\/library\/?$/); expect((await probe(page)).documentToken).toBe(documentToken); await expect(page.locator(".saved-game-card")).toHaveCount(2);
const checkpointA = page.locator(".saved-game-card").filter({ hasText: "Timed checkpoint A" }); await checkpointA.getByRole("link", { name: "Resume", exact: true }).click(); await expect(page).toHaveURL(/\/game\/$/); await expectOneMountedGame(page); await expect(page.locator("#pause")).toHaveText("Resume"); await expect(page.locator("#strikes-stat")).toBeVisible(); await expect(page.locator("#orientation")).toContainText("R2"); expect((await probe(page)).documentToken).toBe(documentToken);
await mainNavigation.getByRole("link", { name: "Library", exact: true }).click(); const checkpointB = page.locator(".saved-game-card").filter({ hasText: "Timed checkpoint B" }); await checkpointB.getByRole("link", { name: "Resume", exact: true }).click(); await expect(page).toHaveURL(/\/game\/$/); await expectOneMountedGame(page); await expect(page.locator("#pause")).toHaveText("Resume"); await expect(page.locator("#orientation")).toContainText("R3"); expect((await probe(page)).documentToken).toBe(documentToken);
await mainNavigation.getByRole("link", { name: "Library", exact: true }).click(); await page.evaluate(() => { window.__migrationProbe!.canvasBeforeResume = document.querySelector<HTMLCanvasElement>("#scene canvas.three-canvas") ?? undefined; }); await page.locator(".saved-game-card").filter({ hasText: "Timed checkpoint B" }) .getByRole("link", { name: "Resume", exact: true }).click(); await page.waitForFunction(() => window.__migrationProbe!.canvasBeforeResume !== document.querySelector("#scene canvas.three-canvas")); await expectOneMountedGame(page); expect((await probe(page)).documentToken).toBe(documentToken);});
test("leaving the game releases its browser resources; Play restores one paused scene", async ({ page }) => { await installBrowserProbe(page); await page.goto("/library/?tab=saved"); await expect(page.getByRole("heading", { name: "Library", exact: true })).toBeVisible(); await expect(page.getByRole("heading", { name: "No saved games yet", exact: true })).toBeVisible(); const baseline = await probe(page);
await page.getByRole("navigation", { name: "Main navigation" }).getByRole("link", { name: "Play", exact: true }).click(); await expect(page).toHaveURL(/\/game\/$/); await expectOneMountedGame(page); const documentToken = (await probe(page)).documentToken; expect(documentToken).toBe(baseline.documentToken); await page.getByRole("button", { name: "Start", exact: true }).click();
await page.locator("#projection-mode").click(); await expect(page.locator("#scene canvas.planar-canvas")).toBeVisible(); await page.waitForFunction(() => window.__migrationProbe!.webglContexts .filter(({ context }) => !context.isContextLost()).length === 2);
await page.getByRole("button", { name: "Audio", exact: true }).click(); await page.waitForFunction(() => window.__migrationProbe!.audioContexts.length === 1); expect((await probe(page)).audioStates).toHaveLength(1);
const mainNavigation = page.getByRole("navigation", { name: "Main navigation" }); await mainNavigation.getByRole("link", { name: "Library", exact: true }).click(); await page.getByRole("navigation", { name: "Library sections" }).getByRole("link", { name: "Scenarios", exact: true }).click(); await expect(page).toHaveURL(/\/library\/\?tab=scenarios$/); await page.waitForFunction((baselineListeners) => { const current = window.__migrationProbe!;
return ( current.frames.size === 0 && current.resizeObservers.size === 0 && current.globalSignalListeners.size === baselineListeners && current.webglContexts.every(({ context }) => context.isContextLost()) && current.audioContexts.every((context) => context.state === "closed") ); }, baseline.globalSignalListeners);
const away = await probe(page); expect(away.documentToken).toBe(documentToken); expect(away.frames).toBe(0); expect(away.resizeObservers).toBe(0); expect(away.globalSignalListeners).toBe(baseline.globalSignalListeners); expect(away.activeWebGLContexts).toBe(0); expect(away.connectedWebGLContexts).toBe(0); expect(away.sceneCanvases).toBe(0); expect(away.audioStates).toEqual(["closed"]);
await mainNavigation.getByRole("link", { name: "Play", exact: true }).click(); await expect(page).toHaveURL(/\/game\/$/); await expectOneMountedGame(page); await expect(page.locator("#pause")).toHaveText("Resume"); const returned = await probe(page); expect(returned.documentToken).toBe(documentToken); expect(returned.activeWebGLContexts).toBe(1); expect(returned.connectedWebGLContexts).toBe(1); expect(returned.resizeObservers).toBe(1); expect(returned.globalSignalListeners).toBeGreaterThan(baseline.globalSignalListeners);
await page.goBack(); await expect(page).toHaveURL(/\/library\/\?tab=scenarios$/); expect((await probe(page)).documentToken).toBe(documentToken); expect((await probe(page)).sceneCanvases).toBe(0);});
test("the scenario library launches a levelled challenge and confirms deletion", async ({ page }) => { await installBrowserProbe(page); await page.goto("/game/?mode=free&seed=1234"); await expectOneMountedGame(page);
const canvas = page.locator("#scene canvas.three-canvas"); await page.getByRole("button", { name: "Select Moniamond", exact: true }).click(); await expect(canvas).toBeFocused(); await page.keyboard.press("Space");
await page.locator("#scenarios-button").click(); await expect(page.locator("#board-scenarios-dialog")).toBeVisible(); await expect(page.locator("#scenarios-status")).toHaveText(""); await expect(page.locator("#scenarios-list")).toHaveCount(0); await page.locator("#scenario-name").fill("Routed Moniamond setup"); await page.locator("#scenario-save").click(); await expect(page.locator("#scenarios-status")).toContainText("Saved"); await page.locator("#board-scenarios-close").click();
const mainNavigation = page.getByRole("navigation", { name: "Main navigation" }); await mainNavigation.getByRole("link", { name: "Library", exact: true }).click(); await page.getByRole("navigation", { name: "Library sections" }).getByRole("link", { name: "Scenarios", exact: true }).click(); await expect(page).toHaveURL(/\/library\/\?tab=scenarios$/); await expect(page.getByRole("heading", { name: "Library", exact: true })).toBeVisible();
const scenario = page.locator(".scenario-card").filter({ hasText: "Routed Moniamond setup" }); await expect(scenario).toHaveCount(1); await expect(scenario.getByRole("heading", { name: "Routed Moniamond setup", exact: true })).toBeVisible(); const timestamp = scenario.locator("time"); await expect(timestamp).toHaveAttribute("datetime", /^\d{4}-\d{2}-\d{2}T/); const savedAt = await timestamp.getAttribute("datetime"); expect(Number.isFinite(new Date(savedAt ?? "").getTime())).toBe(true);
const level = scenario.getByRole("spinbutton", { name: "Challenge starting level" }); await level.fill("100"); expect(await level.evaluate((element) => element instanceof HTMLInputElement && !element.validity.valid && element.validity.rangeOverflow, )).toBe(true); await scenario.getByRole("button", { name: "Timed challenge", exact: true }).click(); await expect(page).toHaveURL(/\/library\/\?tab=scenarios$/);
await level.fill("3"); expect(await level.evaluate((element) => element instanceof HTMLInputElement && element.validity.valid, )).toBe(true); await scenario.getByRole("button", { name: "Timed challenge", exact: true }).click(); await expect(page).toHaveURL(/\/game\/$/); await expectOneMountedGame(page); await expect(page.locator("#strikes-stat")).toBeVisible(); await expect(page.locator("#free-play-tools")).toBeHidden(); await expect(page.locator("#level")).toHaveText("3");
await page.goBack(); await expect(page).toHaveURL(/\/library\/\?tab=scenarios$/); const returnedScenario = page.locator(".scenario-card").filter({ hasText: "Routed Moniamond setup" }); await expect(returnedScenario).toHaveCount(1); await returnedScenario.getByRole("button", { name: "Delete", exact: true }).click(); await returnedScenario.getByRole("button", { name: "Delete scenario", exact: true }).click(); await expect(page.locator(".scenario-card")).toHaveCount(0); await expect(page.getByRole("status")).toContainText("Deleted Routed Moniamond setup.");});