← back to Games Agentabrams

tests/new-games-2.mjs

85 lines

// Headless smoke test for the 2026-07-24 batch (pong, emoji-memory, spirograph,
// constellation-clock). Loads each standalone in a real Chromium page, drives a
// few inputs, and asserts it renders and throws no console/page errors.
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();
}

// --- pong: serve, move both paddles, confirm canvas + overlay gone ---
await run('pong', '/games/pong/', async (page) => {
  await page.keyboard.press('Space');                       // serve
  for (const k of ['w', 's', 'ArrowUp', 'ArrowDown']) { await page.keyboard.down(k); await page.waitForTimeout(120); await page.keyboard.up(k); }
}, async (page) =>
  (await page.locator('canvas#c').count()) === 1 &&
  await page.locator('#msg').evaluate(el => el.classList.contains('hidden'))
);

// --- emoji-memory: flip two cards, confirm board built + moves increments ---
await run('emoji-memory', '/games/emoji-memory/', async (page) => {
  const cards = page.locator('.card');
  await cards.nth(0).click(); await page.waitForTimeout(120);
  await cards.nth(1).click(); await page.waitForTimeout(200);
}, async (page) => {
  const n = await page.locator('.card').count();            // 4x4 default = 16
  const moves = parseInt(await page.locator('#moves').textContent(), 10);
  return n === 16 && moves >= 1;
});

// --- spirograph: generative toy — just confirm it renders a canvas cleanly ---
await run('spirograph', '/games/spirograph/', async (page) => {
  await page.waitForTimeout(400);
}, async (page) => (await page.locator('canvas').count()) >= 1);

// --- constellation-clock: ambient visualizer — canvas renders, no errors ---
await run('constellation-clock', '/games/constellation-clock/', async (page) => {
  await page.waitForTimeout(400);
}, async (page) => (await page.locator('canvas').count()) >= 1);

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);