← back to Games Agentabrams
tests/new-games.mjs
80 lines
// Headless smoke test for the newer games (snake, breakout, 2048).
// Loads each standalone in a real Chromium page, drives a few inputs, and
// asserts it renders and throws no console/page errors. Serves over http
// because several games fetch localStorage/AudioContext that dislike file://.
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import path from 'path';
import http from 'http';
import fs from 'fs';
const require = createRequire(import.meta.url);
let chromium;
try {
({ chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright'));
} catch {
({ chromium } = require('playwright'));
}
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const MIME = { '.html': 'text/html', '.json': 'application/json', '.png': 'image/png', '.js': 'text/javascript' };
const server = http.createServer((req, res) => {
let p = decodeURIComponent(req.url.split('?')[0].split('#')[0]);
if (p.endsWith('/')) p += 'index.html';
const f = path.join(root, p);
if (!f.startsWith(root) || !fs.existsSync(f) || fs.statSync(f).isDirectory()) { res.writeHead(404); res.end('nf'); return; }
res.writeHead(200, { 'Content-Type': MIME[path.extname(f)] || 'text/plain' });
fs.createReadStream(f).pipe(res);
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
const base = `http://127.0.0.1:${server.address().port}`;
const browser = await chromium.launch();
const results = [];
const check = (name, ok) => { results.push({ name, ok }); console.log((ok ? 'PASS' : 'FAIL') + ' ' + name); };
async function run(id, urlPath, drive, assertFn) {
const page = await browser.newPage({ viewport: { width: 700, height: 760 } });
const errors = [];
page.on('pageerror', e => errors.push('pageerror: ' + e.message));
page.on('console', m => { if (m.type() === 'error') errors.push('console: ' + m.text()); });
await page.goto(base + urlPath);
await drive(page);
await page.waitForTimeout(300);
const extraOk = await assertFn(page);
check(`${id}: renders + interactive`, extraOk);
check(`${id}: no console/page errors`, errors.length === 0);
if (errors.length) console.log(' ' + errors.join('\n '));
await page.close();
}
// --- snake: press Play, steer, confirm canvas + a running loop (no error) ---
await run('snake', '/games/snake/', async (page) => {
await page.click('#playBtn');
for (const k of ['ArrowUp', 'ArrowRight', 'ArrowDown', 'ArrowLeft']) { await page.keyboard.press(k); await page.waitForTimeout(120); }
}, async (page) => (await page.locator('canvas#c').count()) === 1 && await page.locator('#ov').evaluate(el => el.classList.contains('hide')));
// --- breakout: Play, launch, move paddle, confirm canvas + overlay hidden ---
await run('breakout', '/games/breakout/', async (page) => {
await page.click('#playBtn');
await page.keyboard.press('Space');
await page.keyboard.down('ArrowRight'); await page.waitForTimeout(200); await page.keyboard.up('ArrowRight');
await page.waitForTimeout(300);
}, async (page) => (await page.locator('canvas#c').count()) === 1 && await page.locator('#ov').evaluate(el => el.classList.contains('hide')));
// --- 2048: starts with 2 tiles; a move keeps a valid board and score >= 0 ---
await run('2048', '/games/2048/', async (page) => {
for (const k of ['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown']) { await page.keyboard.press(k); await page.waitForTimeout(120); }
}, async (page) => {
const tiles = await page.locator('#board .tile').count(); // always 16 cells
const filled = await page.locator('#board .tile').evaluateAll(els => els.filter(e => e.textContent.trim() !== '').length);
const score = parseInt(await page.locator('#score').textContent(), 10);
return tiles === 16 && filled >= 2 && score >= 0;
});
await browser.close();
server.close();
const failed = results.filter(r => !r.ok);
console.log(`\n${results.length - failed.length}/${results.length} passed`);
process.exit(failed.length ? 1 : 0);