clone

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

Exercise stacked rules and contact precedence in browser

Dami committed 6f8cc4b
commit
6f8cc4b30da0ebf6ea2cb49540e3dfad1d6fffa1
parent
364e659b5c46226ebda4975fc1128245fd084d65
author
Dami <dami.etoile@gmail.com>
1 file changed, +121 -0
Atests/browser/rule-reading.mjs121++++++++++++++++++++++++++++++
A tests/browser/rule-reading.mjs +121 -0
@@ -0,0 +1,121 @@
1+#!/usr/bin/env node2+3+/** Browser acceptance for W6 contact precedence and stacked rule reading. */4+5+import assert from 'node:assert/strict';6+import { mkdir, writeFile } from 'node:fs/promises';7+import { dirname, resolve } from 'node:path';8+import { fileURLToPath } from 'node:url';9+10+const projectRoot = resolve(dirname(fileURLToPath(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 playwright = await import(playwrightModule);16+const { chromium } = playwright.default || playwright;17+const baseUrl = process.env.MIAU_URL18+ || process.argv.find(argument => argument.startsWith('--url='))?.slice(6)19+ || 'http://127.0.0.1:5173/';20+const devUrl = (() => { const value = new URL(baseUrl); value.searchParams.set('dev', '1'); value.hash = ''; return value.toString(); })();21+const screenshotDir = '/private/tmp/miau-stage-qa';22+const fixturePath = '/private/tmp/w6-rule-reading.cjs';23+24+const 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+};`;43+44+await mkdir(screenshotDir, { recursive: true });45+await writeFile(fixturePath, fixture);46+const browser = await chromium.launch({ headless: !process.argv.includes('--headed') });47+const errors = [];48+const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });49+const page = await context.newPage();50+page.on('console', message => { if (message.type() === 'error') errors.push(`console: ${message.text()}`); });51+page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));52+53+async function scene(name) {54+ await page.waitForFunction(value => document.querySelector('.stage-shell')?.dataset.scene === value, name, { timeout: 10_000 });55+}56+async function state() {57+ return page.evaluate(() => JSON.parse(window.render_game_to_text()));58+}59+function report(message) { process.stdout.write(`✓ ${message}\n`); }60+61+try {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');72+73+ 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');85+86+ 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');98+99+ // 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+}