← back to Games Agentabrams
tests/popout.mjs
96 lines
// Verify every game is "openable in its own window": each row exposes a real
// <a> pop-out with the correct href, the stage pop-out is a real link, and a
// plain click actually spawns a new browser window/tab. Focuses on rope-physics
// but checks all games have a valid, resolvable pop-out href.
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 ctx = await browser.newContext();
const page = await ctx.newPage();
let fail = 0;
const ok = (c, m) => { console.log((c ? 'PASS' : 'FAIL') + ' — ' + m); if (!c) fail++; };
await page.goto(base + '#rope-physics', { waitUntil: 'networkidle' });
await page.waitForSelector('.game .popout');
// dismiss the fireworks hero splash (z-index 9999) so it can't intercept clicks
await page.keyboard.press('Enter');
await page.waitForSelector('#hero', { state: 'hidden', timeout: 5000 }).catch(() => {});
// 1. every row pop-out is an anchor with an href that resolves (HTTP 200)
const hrefs = await page.$$eval('.game .popout', els => els.map(a => ({ tag: a.tagName, href: a.getAttribute('href'), target: a.getAttribute('target') })));
ok(hrefs.length >= 30, `found ${hrefs.length} game pop-out links`);
ok(hrefs.every(h => h.tag === 'A'), 'every pop-out is an <a> element');
ok(hrefs.every(h => h.target === '_blank'), 'every pop-out has target=_blank');
let bad = 0;
for (const h of hrefs) {
const r = await ctx.request.get(new URL(h.href, base).href);
if (r.status() !== 200) { bad++; console.log(' broken href:', h.href, r.status()); }
}
ok(bad === 0, `all ${hrefs.length} pop-out targets return HTTP 200`);
// 2. stage "own window" control is an anchor pointing at the selected game
const cur = await page.$eval('#popCurrent', a => ({ tag: a.tagName, href: a.getAttribute('href'), target: a.getAttribute('target') }));
ok(cur.tag === 'A' && cur.target === '_blank', 'stage pop-out is an <a target=_blank>');
ok(/games\/rope-physics\/index\.html$/.test(cur.href), 'stage pop-out points at rope-physics: ' + cur.href);
// 3. a plain click on the stage pop-out actually opens a new window
const [popup] = await Promise.all([
ctx.waitForEvent('page', { timeout: 4000 }).catch(() => null),
page.click('#popCurrent'),
]);
ok(!!popup, 'clicking "own window" opened a new browser window');
if (popup) {
await popup.waitForLoadState('domcontentloaded').catch(() => {});
ok(/rope-physics/.test(popup.url()), 'new window loaded the rope-physics game: ' + popup.url());
}
// 4. a plain click on a SIDEBAR pop-out actually opens a new window too
// (guards against the theory that stopPropagation() on the row kills the
// anchor's fallback navigation — it does not: stopPropagation != preventDefault)
const [sidePopup] = await Promise.all([
ctx.waitForEvent('page', { timeout: 4000 }).catch(() => null),
page.click('.game .popout'), // first row's pop-out anchor
]);
ok(!!sidePopup, 'clicking a sidebar pop-out opened a new browser window');
if (sidePopup) {
await sidePopup.waitForLoadState('domcontentloaded').catch(() => {});
ok(/\/games\/.+\//.test(sidePopup.url()), 'sidebar pop-out loaded a standalone game page: ' + sidePopup.url());
}
// 5. a11y: keyboard focus on a sidebar pop-out makes it visible (was opacity:0).
// Wait out the .15s opacity transition before sampling (it animates 0 -> .8).
await page.evaluate(() => { document.querySelector('.game .popout').focus(); });
await page.waitForFunction(
() => parseFloat(getComputedStyle(document.querySelector('.game .popout')).opacity) > 0.5,
{ timeout: 1000 }
).catch(() => {});
const focusedOpacity = await page.evaluate(() => getComputedStyle(document.querySelector('.game .popout')).opacity);
ok(parseFloat(focusedOpacity) > 0.5, `keyboard-focused pop-out is visible (opacity ${focusedOpacity}, was 0)`);
await browser.close();
server.close();
console.log(fail ? `\n${fail} FAILURE(S)` : '\nALL GREEN');
process.exit(fail ? 1 : 0);