- commit
- 8a3dd6e1f880612ebe50c623aaec79a7a5129c57
- parent
- 93264750bf46b674453c601f3cba57c00ee9bb1f
- author
- Dami <dami.etoile@gmail.com>
| A | tests/ | 108 | ++++++++++++++++++++++++++++++ |
@@ -0,0 +1,108 @@
1+2+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+}