- commit
- 6f8cc4b30da0ebf6ea2cb49540e3dfad1d6fffa1
- parent
- 364e659b5c46226ebda4975fc1128245fd084d65
- author
- Dami <dami.etoile@gmail.com>
| A | tests/ | 121 | ++++++++++++++++++++++++++++++ |
@@ -0,0 +1,121 @@
1+2+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+}