clone

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

main
miau-is-true / tests / browser / long-title.mjs
long-title.mjs 8.2K
1#!/usr/bin/env node23/** Long authored title acceptance for public INDEX, proof HUD, and Win card layout. */4import assert from 'node:assert/strict';5import { mkdtemp, rm, writeFile } from 'node:fs/promises';6import { tmpdir } from 'node:os';7import { resolve } from 'node:path';89const configuredPlaywright = process.env.PLAYWRIGHT_MODULE || 'playwright';10const playwrightModule = configuredPlaywright.startsWith('/')11 ? `${configuredPlaywright.replace(/\/$/, '')}${configuredPlaywright.endsWith('.js') ? '' : '/index.js'}`12 : configuredPlaywright;13const imported = await import(playwrightModule);14const { chromium } = imported.default || imported;15const baseUrl = process.env.MIAU_URL || 'http://127.0.0.1:5199/';16const screenshotDir = '/private/tmp/miau-stage-qa';17const authoredTitle = 'A Very Long Authored Proof Title for Narrow Screens';18const fixture = `module.exports = {19 manifest: { id: 'long-title-qa', name: 'Long Title QA', version: '1.0.0', author: 'Browser QA' },20 vocabulary: ['MIAU', 'FLAG'],21 palette: { MIAU: '🐈', FLAG: '🚩' },22 levels: [{23 id: 'long-title-proof', title: ${JSON.stringify(authoredTitle)},24 hint: 'One careful step reaches the flag.', width: 4, height: 3,25 objects: [26 { kind: 'word', text: 'MIAU', x: 0, y: 0 },27 { kind: 'word', text: 'IS', x: 1, y: 0 },28 { kind: 'word', text: 'YOU', x: 2, y: 0 },29 { kind: 'word', text: 'FLAG', x: 0, y: 1 },30 { kind: 'word', text: 'IS', x: 1, y: 1 },31 { kind: 'word', text: 'WIN', x: 2, y: 1 },32 { kind: 'object', noun: 'MIAU', x: 0, y: 2 },33 { kind: 'object', noun: 'FLAG', x: 1, y: 2 }34 ]35 }]36};`;3738const tempDir = await mkdtemp(`${tmpdir()}/miau-long-title-`);39const fixturePath = resolve(tempDir, 'long-title-qa.cjs');40await writeFile(fixturePath, fixture);41const browser = await chromium.launch({ headless: true });42const errors = [];4344async function waitScene(page, expected) {45 await page.waitForFunction(value => document.querySelector('.stage-shell')?.dataset.scene === value, expected, { timeout: 10000 });46}4748async function canvasMetrics(page) {49 return page.evaluate(() => {50 const canvas = document.querySelector('.stage-canvas')?.getBoundingClientRect();51 return {52 scene: document.querySelector('.stage-shell')?.dataset.scene,53 scrollHeight: document.scrollingElement?.scrollHeight,54 innerWidth,55 innerHeight,56 canvas: canvas && { x: canvas.x, y: canvas.y, width: canvas.width, height: canvas.height },57 state: JSON.parse(window.render_game_to_text()),58 };59 });60}6162async function assertViewport(page, label) {63 const metrics = await canvasMetrics(page);64 assert.equal(metrics.scrollHeight, metrics.innerHeight, `${label}: page must not scroll`);65 assert.deepEqual(metrics.canvas, {66 x: 0, y: 0, width: metrics.innerWidth, height: metrics.innerHeight,67 }, `${label}: canvas must cover viewport`);68 return metrics;69}7071try {72 const setupContext = await browser.newContext({ viewport: { width: 1280, height: 800 } });73 const setup = await setupContext.newPage();74 await setup.goto(`${baseUrl.replace(/\/$/, '')}/?dev=1`, { waitUntil: 'networkidle' });75 await setup.keyboard.press('Enter');76 await waitScene(setup, 'index');77 await setup.locator('#mod-input').setInputFiles(fixturePath);78 await waitScene(setup, 'gift');79 await setup.keyboard.press('Enter');80 await waitScene(setup, 'index');81 await setup.locator('[data-pack-id="long-title-qa"]').click();82 await setup.locator('#copy-share').click();83 const shareUrl = await setup.locator('#share-url-input').inputValue();84 assert.ok(shareUrl.includes('#pack='), 'setup should create a data-only share URL');85 await setupContext.close();8687 for (const [name, viewport] of [88 ['desktop', { width: 1280, height: 800 }],89 ['mobile', { width: 390, height: 844 }],90 ]) {91 const context = await browser.newContext({ viewport, isMobile: name === 'mobile', hasTouch: name === 'mobile' });92 const page = await context.newPage();93 await page.addInitScript(() => {94 window.__miauTextCalls = [];95 const original = CanvasRenderingContext2D.prototype.fillText;96 CanvasRenderingContext2D.prototype.fillText = function(value, x, y, ...rest) {97 if (typeof value === 'string') window.__miauTextCalls.push({ value, x, y, font: this.font, align: this.textAlign });98 return original.call(this, value, x, y, ...rest);99 };100 });101 page.on('console', message => { if (message.type() === 'error') errors.push(`${name}: console: ${message.text()}`); });102 page.on('pageerror', error => errors.push(`${name}: page: ${error.message}`));103 await page.goto(shareUrl, { waitUntil: 'networkidle' });104 await waitScene(page, 'gift');105 await page.keyboard.press('Enter');106 await waitScene(page, 'index');107 await page.waitForTimeout(700);108 assert.match(await page.locator('.stage-canvas').getAttribute('aria-label') || '', /Press Enter to enter/, `${name}: first entry shows a world card`);109 await page.keyboard.press('Enter');110 await page.waitForTimeout(100);111 let metrics = await assertViewport(page, `${name} INDEX`);112 assert.equal(metrics.state.level, authoredTitle, `${name}: INDEX must retain authored title`);113 await page.screenshot({ path: `${screenshotDir}/long-title-${name}-index.png` });114115 await page.keyboard.press('Enter');116 await waitScene(page, 'proof');117 await page.waitForTimeout(700);118 await page.evaluate(() => {119 window.__miauTextCalls = [];120 window.dispatchEvent(new Event('resize'));121 });122 await page.waitForTimeout(100);123 metrics = await assertViewport(page, `${name} proof`);124 assert.equal(metrics.state.level, authoredTitle, `${name}: proof must retain authored title`);125 assert.equal(metrics.state.moves, 0, `${name}: proof starts at zero moves`);126 const proofText = await page.evaluate(() => window.__miauTextCalls);127 const titleWords = authoredTitle.split(/\s+/);128 assert.ok(titleWords.every(word => proofText.some(call => call.value === word || call.value.includes(word))), `${name}: every title word remains visible`);129 assert.equal(proofText.some(call => call.value.includes('…')), false, `${name}: authored title has no ellipsis`);130 if (name === 'mobile') {131 const steps = proofText.find(call => call.value === 'STEPS');132 const proofLine = proofText.find(call => call.value.startsWith('PROOF '));133 assert.ok(steps && proofLine, 'mobile: compact counter and proof subline are both drawn');134 assert.equal(Math.round(steps.y), Math.round(proofLine.y + 8), 'mobile: STEPS shares the proof subline row');135 const titleCalls = proofText.filter(call => titleWords.some(word => call.value.includes(word)) && call.font.includes('16px') && call.align === 'left');136 assert.ok(titleCalls.length >= 2 && titleCalls.length <= 3, 'mobile: full-width fallback uses at most three 16px lines');137 const measured = await page.evaluate(() => {138 const canvas = document.querySelector('.stage-canvas');139 const ctx = canvas.getContext('2d');140 return window.__miauTextCalls.filter(call => call.font.includes('16px') && call.value.length > 0).map(call => {141 ctx.font = call.font;142 return { value: call.value, right: call.x + ctx.measureText(call.value).width };143 });144 });145 assert.ok(measured.filter(call => titleWords.some(word => call.value.includes(word))).every(call => call.right <= 374), 'mobile: title lines clear the viewport inset and counter row');146 }147 await page.screenshot({ path: `${screenshotDir}/long-title-${name}-proof.png` });148149 await page.keyboard.press('ArrowRight');150 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).mode === 'won', null, { timeout: 10000 });151 await waitScene(page, 'win');152 await page.waitForTimeout(1400);153 metrics = await assertViewport(page, `${name} Win`);154 assert.equal(metrics.state.mode, 'won', `${name}: Win card follows the one-step proof`);155 await page.screenshot({ path: `${screenshotDir}/long-title-${name}-win.png` });156 await context.close();157 }158159 assert.deepEqual(errors, [], 'long-title flow should have no browser errors');160 console.log(`✓ long authored title fits INDEX/proof/Win at desktop and mobile (${authoredTitle.length} chars)`);161} finally {162 await browser.close();163 await rm(tempDir, { recursive: true, force: true });164}