clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / tests / browser / automata.mjs
automata.mjs 10.0K
1#!/usr/bin/env node23/**4 * Focused browser acceptance for the unbundled automata example pack.5 *6 * Usage:7 * PLAYWRIGHT_MODULE=/Users/dami/.codex/skills/develop-web-game/node_modules/playwright \8 * node tests/browser/automata.mjs --url=http://127.0.0.1:5173/9 */1011import assert from 'node:assert/strict';12import { mkdir } from 'node:fs/promises';13import { dirname, resolve } from 'node:path';14import { fileURLToPath } from 'node:url';1516const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');17const modPath = resolve(projectRoot, 'mods/automata-example.cjs');18const configuredPlaywright = process.env.PLAYWRIGHT_MODULE || 'playwright';19const playwrightModule = configuredPlaywright.startsWith('/')20 ? `${configuredPlaywright.replace(/\/$/, '')}${configuredPlaywright.endsWith('.js') ? '' : '/index.js'}`21 : configuredPlaywright;22const playwright = await import(playwrightModule);23const { chromium } = playwright.default || playwright;24const baseUrl = process.env.MIAU_URL25 || process.argv.find(argument => argument.startsWith('--url='))?.slice(6)26 || 'http://127.0.0.1:5173/';27const devUrl = (() => { const value = new URL(baseUrl); value.searchParams.set('dev', '1'); value.hash = ''; return value.toString(); })();28const screenshots = '/private/tmp/miau-stage-qa';29const timeout = Number(process.env.MIAU_BROWSER_TIMEOUT || 10_000);3031await mkdir(screenshots, { recursive: true });32const browser = await chromium.launch({ headless: !process.argv.includes('--headed') });33const consoleErrors = [];34const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });35const page = await context.newPage();36page.on('console', message => { if (message.type() === 'error') consoleErrors.push(`console: ${message.text()}`); });37page.on('pageerror', error => consoleErrors.push(`pageerror: ${error.message}`));3839async function scene(expected) {40 await page.waitForFunction(value => document.querySelector('.stage-shell')?.dataset.scene === value, expected, { timeout });41}42async function state() {43 return page.evaluate(() => JSON.parse(window.render_game_to_text()));44}45async function screenshot(name) {46 await page.waitForTimeout(750);47 const path = `${screenshots}/${name}`;48 await page.screenshot({ path });49 return path;50}51function check(label) { process.stdout.write(`✓ ${label}\n`); }5253try {54 await page.goto(devUrl, { waitUntil: 'networkidle' });55 await page.waitForSelector('#game-canvas', { state: 'visible', timeout });5657 // Import through the same trusted local-module path a player uses.58 await page.locator('#mod-input').setInputFiles(modPath);59 await scene('gift');60 assert.match(await page.locator('body').innerText(), /A GIFT FOR YOU|KEEP AND PLAY/i);61 check('local automata module opens Gift');62 await page.keyboard.press('Enter');63 await scene('index');64 check('Gift Keep installs and returns to INDEX');6566 // The dev catalog exposes the generated data-only URL after install.67 const pack = page.locator('#pack-list [data-pack-id="automata-example"]');68 await pack.waitFor({ state: 'visible', timeout });69 await pack.click();70 await page.locator('#copy-share').click();71 const shareUrl = await page.locator('#share-url-input').inputValue();72 assert.match(shareUrl, /#pack=/, 'share URL should carry an encoded data-only fragment');73 check('encoded data-only share URL generated');7475 await page.keyboard.press('Enter');76 await scene('proof');77 let current = await state();78 assert.equal(current.level, 'The fox keeps time');79 assert.equal(current.moves, 0);80 assert.equal(current.automata[0].id, 'fenn-clock');81 assert.equal(current.automata[0].property, 'TICK');82 assert.equal(current.automata[0].phase, 0);83 assert.equal(current.automata[0].active, true);84 const fenn = current.objects.find(object => object.kind === 'object' && object.noun === 'FENN');85 assert.deepEqual({ x: fenn.x, y: fenn.y }, { x: 4, y: 3 });86 await screenshot('automata-desktop-initial.png');87 check('desktop proof starts with FENN at (4,3), phase 0');8889 // This pack deliberately repeats MIAU's 🐈 as a plain palette string.90 // A successful right step must still use the cat's native-left face data.91 await page.keyboard.press('ArrowRight');92 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });93 current = await state();94 assert.deepEqual(95 { x: current.objects.find(object => object.kind === 'object' && object.noun === 'MIAU').x,96 y: current.objects.find(object => object.kind === 'object' && object.noun === 'MIAU').y },97 { x: 1, y: 3 },98 );99 await screenshot('automata-desktop-miau-right.png');100 await page.keyboard.press('ArrowLeft');101 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 2, { timeout });102 await screenshot('automata-desktop-miau-left.png');103 await page.keyboard.press('z');104 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });105 await page.keyboard.press('z');106 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 0, { timeout });107 check('desktop MIAU turns right then left with a plain-string cat palette, and Undo restores both turns');108109 await page.keyboard.press('Space');110 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });111 current = await state();112 const movedFenn = current.objects.find(object => object.kind === 'object' && object.noun === 'FENN');113 assert.equal(current.automata[0].phase, 1);114 assert.deepEqual({ x: movedFenn.x, y: movedFenn.y }, { x: 3, y: 3 });115 await screenshot('automata-desktop-wait.png');116 check('desktop Space WAIT advances phase and moves FENN left');117118 await page.keyboard.press('z');119 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 0, { timeout });120 current = await state();121 const undoneFenn = current.objects.find(object => object.kind === 'object' && object.noun === 'FENN');122 assert.equal(current.automata[0].phase, 0);123 assert.deepEqual({ x: undoneFenn.x, y: undoneFenn.y }, { x: 4, y: 3 });124 check('undo restores phase 0 and FENN position');125126 await page.keyboard.press('r');127 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 0, { timeout });128 current = await state();129 assert.equal(current.automata[0].phase, 0);130 check('reset keeps automata at phase 0');131132 // A fresh page proves the URL is self-contained and avoids catalog state leakage.133 const mobile = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });134 const mobileErrors = [];135 const mobilePage = await mobile.newPage();136 mobilePage.on('console', message => { if (message.type() === 'error') mobileErrors.push(`console: ${message.text()}`); });137 mobilePage.on('pageerror', error => mobileErrors.push(`pageerror: ${error.message}`));138 await mobilePage.goto(shareUrl, { waitUntil: 'networkidle' });139 await mobilePage.waitForSelector('#game-canvas', { state: 'attached', timeout });140 await mobilePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'gift', { timeout });141 await mobilePage.keyboard.press('Enter');142 await mobilePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'index', { timeout });143 await mobilePage.keyboard.press('Enter');144 await mobilePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'proof', { timeout });145 let mobileState = await mobilePage.evaluate(() => JSON.parse(window.render_game_to_text()));146 assert.equal(mobileState.automata[0].phase, 0);147 const initialMobileFenn = mobileState.objects.find(object => object.kind === 'object' && object.noun === 'FENN');148 assert.deepEqual({ x: initialMobileFenn.x, y: initialMobileFenn.y }, { x: 4, y: 3 });149 await mobilePage.waitForTimeout(750);150 await mobilePage.screenshot({ path: `${screenshots}/automata-mobile-initial.png` });151 await mobilePage.keyboard.press('ArrowRight');152 await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });153 await mobilePage.waitForTimeout(750);154 await mobilePage.screenshot({ path: `${screenshots}/automata-mobile-miau-right.png` });155 await mobilePage.keyboard.press('ArrowLeft');156 await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 2, { timeout });157 await mobilePage.waitForTimeout(750);158 await mobilePage.screenshot({ path: `${screenshots}/automata-mobile-miau-left.png` });159 await mobilePage.keyboard.press('z');160 await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });161 await mobilePage.keyboard.press('z');162 await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 0, { timeout });163 check('mobile MIAU right and left frames and Undo complete');164 await mobilePage.mouse.click(195, 770);165 await mobilePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });166 mobileState = await mobilePage.evaluate(() => JSON.parse(window.render_game_to_text()));167 assert.equal(mobileState.automata[0].phase, 1);168 const mobileFenn = mobileState.objects.find(object => object.kind === 'object' && object.noun === 'FENN');169 assert.deepEqual({ x: mobileFenn.x, y: mobileFenn.y }, { x: 3, y: 3 });170 await mobilePage.waitForTimeout(750);171 await mobilePage.screenshot({ path: `${screenshots}/automata-mobile-wait.png` });172 check('mobile WAIT control advances phase and moves FENN');173 assert.deepEqual(mobileErrors, [], 'mobile browser should have no console/page errors');174 await mobile.close();175176 assert.deepEqual(consoleErrors, [], 'desktop browser should have no console/page errors');177 check('desktop and mobile complete without console errors');178 process.stdout.write(`shareUrl=${shareUrl}\n`);179 process.stdout.write(`screenshots=${screenshots}/automata-desktop-miau-right.png,${screenshots}/automata-desktop-miau-left.png,${screenshots}/automata-mobile-miau-right.png,${screenshots}/automata-mobile-miau-left.png\n`);180} finally {181 await context.close();182 await browser.close();183}