clone

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

Cover blocked and transferred automata beats in browser

Dami committed 8a3dd6e
commit
8a3dd6e1f880612ebe50c623aaec79a7a5129c57
parent
93264750bf46b674453c601f3cba57c00ee9bb1f
author
Dami <dami.etoile@gmail.com>
1 file changed, +108 -0
Atests/browser/automata-edges.mjs108++++++++++++++++++++++++++++++
A tests/browser/automata-edges.mjs +108 -0
@@ -0,0 +1,108 @@
1+#!/usr/bin/env node2+3+/** Narrow browser fixtures for automaton blocking, cues, and property transfer. */4+5+import assert from 'node:assert/strict';6+import { mkdir } from 'node:fs/promises';7+import { resolve } from 'node:path';8+import { fileURLToPath } from 'node:url';9+10+const projectRoot = resolve(fileURLToPath(new URL('../..', import.meta.url)));11+const configuredPlaywright = process.env.PLAYWRIGHT_MODULE || 'playwright';12+const playwrightModule = configuredPlaywright.startsWith('/')13+ ? `${configuredPlaywright.replace(/\/$/, '')}${configuredPlaywright.endsWith('.js') ? '' : '/index.js'}`14+ : configuredPlaywright;15+const importedPlaywright = await import(playwrightModule);16+const { chromium } = importedPlaywright.default || importedPlaywright;17+const { encodeSharedPack } = await import(resolve(projectRoot, 'src/share.js'));18+const baseUrl = process.env.MIAU_URL19+ || process.argv.find(argument => argument.startsWith('--url='))?.slice(6)20+ || 'http://127.0.0.1:5173/';21+const screenshots = '/private/tmp/miau-stage-qa';22+const timeout = Number(process.env.MIAU_BROWSER_TIMEOUT || 10_000);23+24+const fixture = {25+ manifest: { id: 'automata-edge-check', name: 'Automata Edge Check', version: '1.0.0', engine: 1 },26+ vocabulary: ['MIAU', 'MOUSE', 'BELL'],27+ palette: { MIAU: '🐈', MOUSE: '🐭', BELL: '🔔' },28+ levels: [{29+ id: 'block-and-transfer', title: 'Blocked beat and transfer', hint: 'The carrier changes when TICK moves.', width: 8, height: 5,30+ objects: [31+ { kind: 'word', text: 'BELL', x: 0, y: 0 }, { kind: 'word', text: 'IS', x: 1, y: 0 },32+ { kind: 'word', text: 'MIAU', x: 0, y: 4 }, { kind: 'word', text: 'IS', x: 1, y: 4 }, { kind: 'word', text: 'YOU', x: 2, y: 4 },33+ { kind: 'word', text: 'MOUSE', x: 0, y: 1 }, { kind: 'word', text: 'IS', x: 1, y: 1 }, { id: 'tick-word', kind: 'word', text: 'TICK', x: 2, y: 1 },34+ { id: 'cat', kind: 'object', noun: 'MIAU', x: 2, y: 3 }, { id: 'mouse', kind: 'object', noun: 'MOUSE', x: 0, y: 3 }, { id: 'bell', kind: 'object', noun: 'BELL', x: 6, y: 3 },35+ ],36+ automata: [{ id: 'tick-clock', property: 'TICK', pattern: [37+ { action: 'move', dx: -1, dy: 0, cue: 'BLOCK', sound: 'tick' },38+ { action: 'wait', cue: 'NEXT', sound: 'tick' },39+ ] }],40+ }],41+};42+43+const sharedUrl = new URL(baseUrl);44+sharedUrl.search = '';45+sharedUrl.hash = `pack=${encodeSharedPack(fixture)}`;46+await mkdir(screenshots, { recursive: true });47+const browser = await chromium.launch({ headless: !process.argv.includes('--headed') });48+const context = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });49+const page = await context.newPage();50+page.setDefaultTimeout(timeout);51+const consoleErrors = [];52+page.on('console', message => { if (message.type() === 'error' && !/favicon\.ico/i.test(message.text())) consoleErrors.push(message.text()); });53+page.on('pageerror', error => consoleErrors.push(error.message));54+55+async function scene(expected) {56+ await page.waitForFunction(value => document.querySelector('.stage-shell')?.dataset.scene === value, expected, { timeout });57+}58+async function state() { return page.evaluate(() => JSON.parse(window.render_game_to_text())); }59+async function capture(name) { await page.waitForTimeout(500); await page.screenshot({ path: `${screenshots}/${name}` }); }60+function check(label) { process.stdout.write(`✓ ${label}\n`); }61+62+try {63+ await page.goto(sharedUrl.toString(), { waitUntil: 'networkidle' });64+ await scene('gift');65+ await page.keyboard.press('Enter');66+ await scene('index');67+ await page.keyboard.press('Enter');68+ await scene('proof');69+70+ let current = await state();71+ const automaton = () => current.automata.find(system => system.id === 'tick-clock');72+ assert.equal(automaton().active, true);73+ assert.deepEqual(automaton().carriers, [{ noun: 'MOUSE', instanceIds: ['mouse'] }]);74+ assert.deepEqual(automaton().next, { action: 'move', dx: -1, dy: 0, cue: 'BLOCK', sound: 'tick' });75+ await capture('automata-edge-initial-mobile.png');76+ check('initial active carrier exposes current move and cue');77+78+ // MIAU moves up; MOUSE is at the left edge, so the active move is blocked.79+ await page.keyboard.press('ArrowUp');80+ await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 1, { timeout });81+ current = await state();82+ const blockedCarrier = current.objects.find(object => object.id === 'mouse');83+ assert.deepEqual({ x: blockedCarrier.x, y: blockedCarrier.y }, { x: 0, y: 3 });84+ assert.equal(automaton().phase, 1);85+ assert.deepEqual(automaton().next, { action: 'wait', cue: 'NEXT', sound: 'tick' });86+ assert.match(await page.locator('.stage-sr').textContent(), /MOUSE IS TICK/);87+ await capture('automata-edge-blocked-mobile.png');88+ check('blocked automaton action preserves position, advances phase, and exposes next cue');89+90+ // The second up pushes TICK from MOUSE IS TICK into BELL IS TICK.91+ await page.keyboard.press('ArrowUp');92+ await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 2, { timeout });93+ current = await state();94+ assert.deepEqual(automaton().carriers, [{ noun: 'BELL', instanceIds: ['bell'] }]);95+ assert.equal(automaton().active, true);96+ assert.equal(current.objects.find(object => object.id === 'tick-word').y, 0);97+ assert.equal(current.objects.find(object => object.id === 'mouse').x, 0);98+ assert.deepEqual(automaton().next, { action: 'move', dx: -1, dy: 0, cue: 'BLOCK', sound: 'tick' });99+ assert.match(await page.locator('.stage-sr').textContent(), /BELL IS TICK/);100+ await capture('automata-edge-transfer-mobile.png');101+ check('property transfer moves the active carrier from MOUSE to BELL');102+103+ assert.deepEqual(consoleErrors, [], 'automata edge fixture should have no console errors');104+ process.stdout.write(`screenshots=${screenshots}/automata-edge-initial-mobile.png,${screenshots}/automata-edge-blocked-mobile.png,${screenshots}/automata-edge-transfer-mobile.png\n`);105+} finally {106+ await context.close();107+ await browser.close();108+}