← back to Games Agentabrams
tests/e2e.mjs
76 lines
// Headless E2E for the games hub. Needs a static server (fetch of games.json
// fails on file://), so it spins one up on an ephemeral port.
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' };
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 url = `http://127.0.0.1:${server.address().port}/`;
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1200, height: 800 } });
const errors = [];
page.on('pageerror', e => errors.push('pageerror: ' + e.message));
page.on('console', m => { if (m.type() === 'error') errors.push('console: ' + m.text()); });
const results = [];
const check = (name, ok) => { results.push({ name, ok }); console.log((ok ? 'PASS' : 'FAIL') + ' ' + name); };
await page.goto(url);
await page.waitForSelector('.game');
check('game list renders from manifest', await page.locator('.game').count() >= 1);
check('AbramsCube.aa listed', (await page.locator('.game .fname').allTextContents()).includes('AbramsCube.aa'));
check('first game auto-selected', await page.locator('.game.active').count() === 1);
// Deep-link to the cube so the cube-specific checks are order-independent
// (the hub lists newest-first, so the cube is no longer the first game).
await page.goto(url + '#rubiks-cube');
await page.waitForSelector('.game.active');
const frame = page.frameLocator('#frame');
await frame.locator('#scramble').waitFor({ timeout: 15000 });
check('game iframe loads the cube', await frame.locator('.cubie').count() === 26);
// game is actually playable inside the frame
const gameFrame = await (await page.$('#frame')).contentFrame();
await gameFrame.waitForFunction(() => window.__cube && !window.__cube.isBusy());
await gameFrame.evaluate(() => window.__cube.enqueue('U'));
await gameFrame.waitForFunction(() => !window.__cube.isBusy());
check('cube playable inside hub', (await gameFrame.evaluate(() => window.__cube.getMoves())) === 1);
// deep link
await page.goto(url + '#rubiks-cube');
await page.waitForSelector('.game.active');
check('hash deep-link selects game', await page.locator('.game.active').count() === 1);
await page.screenshot({ path: path.join(root, 'tests', 'screenshot.png') });
check('no console/page errors', errors.length === 0);
if (errors.length) console.log(errors.join('\n'));
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);