clone

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

main
miau-is-true / tests / browser / rule-reading.mjs
rule-reading.mjs 6.6K
1#!/usr/bin/env node23/** Browser acceptance for W6 contact precedence and stacked rule reading. */45import assert from 'node:assert/strict';6import { mkdir, writeFile } from 'node:fs/promises';7import { dirname, resolve } from 'node:path';8import { fileURLToPath } from 'node:url';910const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');11const configuredPlaywright = process.env.PLAYWRIGHT_MODULE || 'playwright';12const playwrightModule = configuredPlaywright.startsWith('/')13 ? `${configuredPlaywright.replace(/\/$/, '')}${configuredPlaywright.endsWith('.js') ? '' : '/index.js'}`14 : configuredPlaywright;15const playwright = await import(playwrightModule);16const { chromium } = playwright.default || playwright;17const baseUrl = process.env.MIAU_URL18 || process.argv.find(argument => argument.startsWith('--url='))?.slice(6)19 || 'http://127.0.0.1:5173/';20const devUrl = (() => { const value = new URL(baseUrl); value.searchParams.set('dev', '1'); value.hash = ''; return value.toString(); })();21const screenshotDir = '/private/tmp/miau-stage-qa';22const fixturePath = '/private/tmp/w6-rule-reading.cjs';2324const fixture = `module.exports = {25 manifest: { id: 'w6-rule-reading', name: 'W6 Rule Reading', version: '1.0.0', author: 'QA' },26 vocabulary: ['MIAU', 'BABA', 'ROCK', 'FLAG', 'SKULL'],27 palette: { MIAU: '🐈', BABA: '🧒', ROCK: '🪨', FLAG: '🚩', SKULL: '💀' },28 levels: [{29 id: 'contact-and-stack', title: 'Contact and stack', hint: 'Read the stacked YOU rules, then walk into FLAG and SKULL together.',30 width: 6, height: 5,31 objects: [32 { kind: 'word', text: 'BABA', x: 0, y: 0, id: 'stack-baba' }, { kind: 'word', text: 'ROCK', x: 0, y: 0, id: 'stack-rock' },33 { kind: 'word', text: 'IS', x: 1, y: 0, id: 'stack-is' }, { kind: 'word', text: 'YOU', x: 2, y: 0, id: 'stack-you' },34 { kind: 'word', text: 'MIAU', x: 0, y: 1 }, { kind: 'word', text: 'IS', x: 1, y: 1 }, { kind: 'word', text: 'YOU', x: 2, y: 1 },35 { kind: 'word', text: 'FLAG', x: 0, y: 2 }, { kind: 'word', text: 'IS', x: 1, y: 2 }, { kind: 'word', text: 'WIN', x: 2, y: 2 },36 { kind: 'word', text: 'SKULL', x: 0, y: 3 }, { kind: 'word', text: 'IS', x: 1, y: 3 }, { kind: 'word', text: 'DEFEAT', x: 2, y: 3 },37 { kind: 'object', noun: 'MIAU', x: 0, y: 4, id: 'player' },38 { kind: 'object', noun: 'FLAG', x: 2, y: 4, id: 'flag' },39 { kind: 'object', noun: 'SKULL', x: 2, y: 4, id: 'skull' },40 ],41 }],42};`;4344await mkdir(screenshotDir, { recursive: true });45await writeFile(fixturePath, fixture);46const browser = await chromium.launch({ headless: !process.argv.includes('--headed') });47const errors = [];48const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });49const page = await context.newPage();50page.on('console', message => { if (message.type() === 'error') errors.push(`console: ${message.text()}`); });51page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));5253async function scene(name) {54 await page.waitForFunction(value => document.querySelector('.stage-shell')?.dataset.scene === value, name, { timeout: 10_000 });55}56async function state() {57 return page.evaluate(() => JSON.parse(window.render_game_to_text()));58}59function report(message) { process.stdout.write(`✓ ${message}\n`); }6061try {62 await page.goto(devUrl, { waitUntil: 'networkidle' });63 await page.locator('#mod-input').setInputFiles(fixturePath);64 await scene('gift');65 await page.keyboard.press('Enter');66 await scene('index');67 await page.locator('#pack-list [data-pack-id="w6-rule-reading"]').click();68 await page.locator('#copy-share').click();69 const shareUrl = await page.locator('#share-url-input').inputValue();70 assert.match(shareUrl, /#pack=/);71 report('local fixture installs and produces a data-only shared link');7273 await page.keyboard.press('Enter');74 await scene('proof');75 let current = await state();76 assert.equal(current.level, 'Contact and stack');77 assert.deepEqual(current.rules.map(rule => rule.text), [78 'BABA IS YOU', 'ROCK IS YOU', 'MIAU IS YOU', 'FLAG IS WIN', 'SKULL IS DEFEAT',79 ]);80 assert.deepEqual(current.rules.slice(0, 2).map(rule => rule.sourceIds), [81 ['stack-baba', 'stack-is', 'stack-you'], ['stack-rock', 'stack-is', 'stack-you'],82 ]);83 assert.equal(current.rules.slice(0, 2).every(rule => rule.axis === 'h'), true);84 report('stacked noun words produce separate stable ledger rules');8586 await page.keyboard.press('ArrowRight');87 await page.keyboard.press('ArrowRight');88 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves === 2, { timeout: 10_000 });89 current = await state();90 assert.equal(current.won ?? current.mode === 'won', false);91 assert.equal(current.mode, 'play');92 assert.equal(current.objects.some(object => object.id === 'player'), false);93 assert.equal(current.objects.some(object => object.id === 'flag'), true);94 assert.equal(current.objects.some(object => object.id === 'skull'), true);95 await page.waitForTimeout(700);96 await page.locator('.stage-canvas').screenshot({ path: `${screenshotDir}/w6-contact-precedence.png` });97 report('DEFEAT resolves before WIN on shared contact and removes YOU');9899 // A fresh shared-link page confirms the same authored ledger survives URL transport.100 const shared = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });101 const sharedPage = await shared.newPage();102 const sharedErrors = [];103 sharedPage.on('console', message => { if (message.type() === 'error') sharedErrors.push(`console: ${message.text()}`); });104 sharedPage.on('pageerror', error => sharedErrors.push(`pageerror: ${error.message}`));105 await sharedPage.goto(shareUrl, { waitUntil: 'networkidle' });106 await sharedPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'gift', { timeout: 10_000 });107 await sharedPage.keyboard.press('Enter');108 await sharedPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'index', { timeout: 10_000 });109 await sharedPage.keyboard.press('Enter');110 await sharedPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'proof', { timeout: 10_000 });111 const sharedState = await sharedPage.evaluate(() => JSON.parse(window.render_game_to_text()));112 assert.deepEqual(sharedState.rules.slice(0, 2).map(rule => rule.text), ['BABA IS YOU', 'ROCK IS YOU']);113 assert.deepEqual(sharedErrors, []);114 await shared.close();115 assert.deepEqual(errors, []);116 report('shared-link mobile proof preserves stacked ledger with no browser errors');117 process.stdout.write(`screenshots=${screenshotDir}/w6-contact-precedence.png\n`);118} finally {119 await context.close();120 await browser.close();121}