clone

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

main
miau-is-true / tests / browser / world-index.mjs
world-index.mjs 10.2K
1#!/usr/bin/env node23/** Browser acceptance for D20 world switching and INDEX world cards. */45import assert from 'node:assert/strict';6import { mkdir } from 'node:fs/promises';78const configuredPlaywright = process.env.PLAYWRIGHT_MODULE || 'playwright';9const playwrightModule = configuredPlaywright.startsWith('/')10 ? `${configuredPlaywright.replace(/\/$/, '')}${configuredPlaywright.endsWith('.js') ? '' : '/index.js'}`11 : configuredPlaywright;12const playwright = await import(playwrightModule);13const { chromium } = playwright.default || playwright;14const baseUrl = process.env.MIAU_URL15 || process.argv.find(argument => argument.startsWith('--url='))?.slice(6)16 || 'http://127.0.0.1:5202/';17const playerUrl = (() => {18 const value = new URL(baseUrl);19 value.searchParams.delete('dev');20 value.hash = '';21 return value.toString();22})();23const screenshotDir = '/private/tmp/miau-stage-qa';24await mkdir(screenshotDir, { recursive: true });2526function report(message) { process.stdout.write(`✓ ${message}\n`); }2728async function waitScene(page, name) {29 await page.waitForFunction(value => document.querySelector('.stage-shell')?.dataset.scene === value, name, { timeout: 10_000 });30}3132async function canvasLabel(page) {33 return page.locator('.stage-canvas').getAttribute('aria-label');34}3536async function openIndex(page) {37 await page.goto(playerUrl, { waitUntil: 'networkidle' });38 await page.keyboard.press('Enter');39 await waitScene(page, 'index');40 await page.waitForFunction(() => document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Press Enter to enter'));41}4243async function dismissCard(page) {44 await page.keyboard.press('Enter');45 await page.waitForFunction(() => !document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Press Enter to enter'));46 assert.equal(await page.locator('.stage-shell').getAttribute('data-scene'), 'index');47}4849async function switchWorld(page, key, name, number) {50 await page.keyboard.press(key);51 await page.waitForFunction(value => {52 const label = document.querySelector('.stage-canvas')?.getAttribute('aria-label') || '';53 return label.includes(value) && label.includes('Press Enter to enter');54 }, name);55 const cardLabel = await canvasLabel(page);56 assert.match(cardLabel, new RegExp(`World ${number}|${name}`));57 await page.waitForTimeout(360);58 await page.screenshot({ path: `${screenshotDir}/d20-world-${number}-card.png` });59 await dismissCard(page);60 const settledLabel = await canvasLabel(page);61 assert.match(settledLabel, new RegExp(`World ${number}: ${name}`));62 return settledLabel;63}6465const errors = [];66const browser = await chromium.launch({ headless: !process.argv.includes('--headed') });67const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });68const page = await context.newPage();69page.on('console', message => { if (message.type() === 'error') errors.push(`console: ${message.text()}`); });70page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));7172try {73 await openIndex(page);74 await page.waitForTimeout(360);75 assert.match(await canvasLabel(page), /Miau Core/);76 assert.doesNotMatch(await canvasLabel(page), /unknown author/i);77 await page.screenshot({ path: `${screenshotDir}/d20-world-1-card.png` });78 report('Title enters INDEX with the Core world card');7980 await dismissCard(page);81 assert.match(await canvasLabel(page), /World 1: Miau Core/);82 await page.keyboard.press('Enter');83 await waitScene(page, 'proof');84 report('one Enter dismisses the first-entry card, while a second Enter enters the proof');8586 await page.keyboard.press('Escape');87 await waitScene(page, 'pause');88 await page.keyboard.press('ArrowDown');89 await page.keyboard.press('ArrowDown');90 await page.keyboard.press('Enter');91 await waitScene(page, 'index');92 assert.doesNotMatch(await canvasLabel(page), /Press Enter to enter/);93 assert.match(await canvasLabel(page), /World 1: Miau Core/);94 report('returning to INDEX does not replay the Core world card');9596 await switchWorld(page, 'e', 'Crossword', 2);97 await switchWorld(page, 'e', 'Crate Works', 3);98 const tidewaterLabel = await switchWorld(page, 'e', 'Tidewater', 4);99 assert.match(tidewaterLabel, /0 of \d+ proofs complete/);100 assert.match(tidewaterLabel, /Q and E switch worlds/);101 report('Q/E follows the ordered worlds and exposes per-world progress/accessibility');102103 await page.keyboard.press('e');104 await page.waitForTimeout(80);105 assert.equal(await page.locator('.stage-shell').getAttribute('data-scene'), 'index');106 assert.match(await page.locator('.stage-sr').textContent(), /Already at the last world/);107 await page.keyboard.press('q');108 await page.waitForFunction(() => document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('World 3: Crate Works'));109 await page.keyboard.press('q');110 await page.waitForFunction(() => document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('World 2: Crossword'));111 await page.keyboard.press('q');112 await page.waitForFunction(() => document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('World 1: Miau Core'));113 await page.keyboard.press('q');114 await page.waitForTimeout(80);115 assert.match(await page.locator('.stage-sr').textContent(), /Already at the first world/);116 report('outer worlds stop with accessible boundary feedback and never wrap');117118 const workshopPage = await context.newPage();119 const workshopErrors = [];120 workshopPage.on('console', message => { if (message.type() === 'error') workshopErrors.push(`console: ${message.text()}`); });121 workshopPage.on('pageerror', error => workshopErrors.push(`pageerror: ${error.message}`));122 await workshopPage.goto(playerUrl, { waitUntil: 'networkidle' });123 await workshopPage.keyboard.press(' ');124 await waitScene(workshopPage, 'workshop');125 await workshopPage.keyboard.press('ArrowDown');126 await workshopPage.keyboard.press('Enter');127 await waitScene(workshopPage, 'index');128 await workshopPage.waitForFunction(() => document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Press Enter to enter'));129 await workshopPage.waitForTimeout(360);130 assert.match(await canvasLabel(workshopPage), /Miau Core/);131 await workshopPage.screenshot({ path: `${screenshotDir}/d20-workshop-play-card.png` });132 assert.deepEqual(workshopErrors, []);133 await workshopPage.close();134 report('Workshop PLAY opens the same first-entry card origin');135136 const mobile = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });137 const mobilePage = await mobile.newPage();138 const mobileErrors = [];139 mobilePage.on('console', message => { if (message.type() === 'error') mobileErrors.push(`console: ${message.text()}`); });140 mobilePage.on('pageerror', error => mobileErrors.push(`pageerror: ${error.message}`));141 await openIndex(mobilePage);142 await mobilePage.waitForTimeout(360);143 await mobilePage.screenshot({ path: `${screenshotDir}/d20-world-card-mobile.png` });144 await mobilePage.touchscreen.tap(195, 559);145 await mobilePage.waitForFunction(() => !document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Press Enter to enter'));146 assert.equal(await mobilePage.locator('.stage-shell').getAttribute('data-scene'), 'index');147 await mobilePage.touchscreen.tap(337, 92);148 await mobilePage.waitForFunction(() => document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Crossword') && document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Press Enter to enter'));149 await mobilePage.waitForTimeout(360);150 await mobilePage.screenshot({ path: `${screenshotDir}/d20-world-2-card-mobile.png` });151 await mobilePage.touchscreen.tap(195, 559);152 await mobilePage.waitForFunction(() => !document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Press Enter to enter'));153 await mobilePage.touchscreen.tap(321, 827);154 await waitScene(mobilePage, 'pause');155 assert.deepEqual(mobileErrors, []);156 await mobile.close();157 report('mobile card, world arrow, and menu touch targets work without entering a proof');158159 const mobileWorkshop = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });160 const mobileWorkshopPage = await mobileWorkshop.newPage();161 const mobileWorkshopErrors = [];162 mobileWorkshopPage.on('pageerror', error => mobileWorkshopErrors.push(error.message));163 await mobileWorkshopPage.goto(playerUrl, { waitUntil: 'networkidle' });164 await mobileWorkshopPage.keyboard.press(' ');165 await waitScene(mobileWorkshopPage, 'workshop');166 await mobileWorkshopPage.keyboard.press('ArrowDown');167 await mobileWorkshopPage.keyboard.press('Enter');168 await waitScene(mobileWorkshopPage, 'index');169 await mobileWorkshopPage.waitForFunction(() => document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Press Enter to enter'));170 await mobileWorkshopPage.waitForTimeout(360);171 await mobileWorkshopPage.screenshot({ path: `${screenshotDir}/d20-workshop-play-card-mobile.png` });172 assert.deepEqual(mobileWorkshopErrors, []);173 await mobileWorkshop.close();174 report('mobile Workshop PLAY opens a readable first-entry world card');175176 const reduced = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, reducedMotion: 'reduce' });177 const reducedPage = await reduced.newPage();178 const reducedErrors = [];179 reducedPage.on('pageerror', error => reducedErrors.push(error.message));180 await openIndex(reducedPage);181 await reducedPage.keyboard.press('Enter');182 await reducedPage.waitForFunction(() => !document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Press Enter to enter'));183 await reducedPage.touchscreen.tap(337, 92);184 await reducedPage.waitForFunction(() => document.querySelector('.stage-canvas')?.getAttribute('aria-label')?.includes('Crossword'));185 await reducedPage.screenshot({ path: `${screenshotDir}/d20-reduced-motion-mobile.png` });186 assert.deepEqual(reducedErrors, []);187 await reduced.close();188 report('reduced-motion world switching settles directly on the destination card');189190 assert.deepEqual(errors, []);191 process.stdout.write(`screenshots=${screenshotDir}/d20-world-1-card.png\n`);192} finally {193 await context.close();194 await browser.close();195}