Tambah harness uji headless (Playwright)
This commit is contained in:
parent
e50ead2cd8
commit
7038918998
|
|
@ -0,0 +1,81 @@
|
||||||
|
// Headless test harness for Konoha Fighting (Playwright + Chromium). No game code is changed by running it.
|
||||||
|
// node tests/harness.mjs smoke all 7 characters vs CPU on several levels, collects page errors
|
||||||
|
// node tests/harness.mjs <file.mjs> runs a scenario module: export default async ({ page, game, errors }) => result
|
||||||
|
// Env: PW_CORE (path to playwright-core index.mjs), PORT (default random), HEADED=1.
|
||||||
|
import http from 'node:http';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const PW = process.env.PW_CORE || '/Users/cifo/Documents/Projects/erp/node_modules/playwright-core/index.mjs';
|
||||||
|
const { chromium } = await import(PW);
|
||||||
|
const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.webp': 'image/webp', '.png': 'image/png', '.svg': 'image/svg+xml', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4', '.ttf': 'font/ttf', '.jpg': 'image/jpeg' };
|
||||||
|
|
||||||
|
export function serve(dir = root) {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
let p = decodeURIComponent(new URL(req.url, 'http://x').pathname); if (p.endsWith('/')) p += 'index.html';
|
||||||
|
const f = path.join(dir, p);
|
||||||
|
if (!f.startsWith(dir) || !fs.existsSync(f) || fs.statSync(f).isDirectory()) { res.writeHead(404); res.end('nf'); return; }
|
||||||
|
res.writeHead(200, { 'Content-Type': MIME[path.extname(f)] || 'application/octet-stream', 'Cache-Control': 'no-store' }); fs.createReadStream(f).pipe(res);
|
||||||
|
});
|
||||||
|
return new Promise(r => server.listen(Number(process.env.PORT) || 0, '127.0.0.1', () => r({ server, url: `http://127.0.0.1:${server.address().port}/` })));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function open({ dir = root, query = '?touch=0', viewport = { width: 1280, height: 720 } } = {}) {
|
||||||
|
const { server, url } = await serve(dir);
|
||||||
|
const exe = process.env.CHROME_BIN || [`${process.env.HOME}/Library/Caches/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-mac-x64/chrome-headless-shell`, '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'].find(f => fs.existsSync(f));
|
||||||
|
const browser = await chromium.launch({ executablePath: exe, headless: !process.env.HEADED, args: ['--mute-audio', '--autoplay-policy=no-user-gesture-required'] });
|
||||||
|
const context = await browser.newContext({ viewport, serviceWorkers: 'block' });
|
||||||
|
const page = await context.newPage();
|
||||||
|
const errors = [];
|
||||||
|
page.on('pageerror', e => errors.push('pageerror: ' + e.message));
|
||||||
|
page.on('console', m => { if (m.type() === 'error' && !/Failed to load resource|favicon|net::ERR/.test(m.text())) errors.push('console: ' + m.text()); });
|
||||||
|
await page.goto(url + query);
|
||||||
|
await page.waitForFunction(() => window.__game && window.__game.snapshot().ready, null, { timeout: 60000 });
|
||||||
|
await page.evaluate(() => { document.querySelectorAll('audio,video').forEach(a => { a.muted = true; a.pause(); }); const s = document.createElement('style'); s.textContent = '#pause-panel{display:none!important}'; document.head.append(s); });
|
||||||
|
const game = (fn, arg) => page.evaluate(fn, arg);
|
||||||
|
const close = async () => { await browser.close(); server.close(); };
|
||||||
|
return { page, game, errors, close, url };
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHARS = ['browo', 'megachan', 'sonofsolo', 'ethanolman', 'anice', 'soloman', 'gufron'];
|
||||||
|
export const characters = CHARS;
|
||||||
|
|
||||||
|
async function smoke() {
|
||||||
|
const t = await open();
|
||||||
|
const out = [];
|
||||||
|
for (const level of ['easy', 'hard']) for (const [i, p] of CHARS.entries()) {
|
||||||
|
const e = CHARS[(i + 2) % CHARS.length];
|
||||||
|
const r = await t.game(async ({ p, e, level }) => {
|
||||||
|
const g = window.__game; g.startMatch({ player: p, enemy: e, stage: 'bellora', level, mode: 'versus' }); g.setAI(true);
|
||||||
|
for (let i = 0; i < 70; i++) {
|
||||||
|
g.advance(.25);
|
||||||
|
if (i % 5 === 0) { g.press('d'); g.press(' '); g.release(' '); g.release('d'); }
|
||||||
|
if (i % 13 === 0) { g.press('p'); g.release('p'); }
|
||||||
|
if (i % 8 === 0) { g.press('i'); g.release('i'); g.press('o'); g.release('o'); }
|
||||||
|
if (i % 9 === 0) { g.press('l'); } if (i % 9 === 4) g.release('l');
|
||||||
|
if (i % 11 === 0) { g.press('s'); g.release('s'); g.press('s'); g.release('s'); g.press(' '); g.release(' '); }
|
||||||
|
}
|
||||||
|
const s = g.snapshot(); return { hp: Math.round(g.hero.hp), dmg: Math.round(g.dummy.damage), phase: s.match?.phase };
|
||||||
|
}, { p, e, level });
|
||||||
|
out.push(`${level} ${p}>${e} hero ${r.hp} rival-dmg ${r.dmg} ${r.phase}`);
|
||||||
|
}
|
||||||
|
await t.close();
|
||||||
|
const bad = t.errors;
|
||||||
|
console.log(out.join('\n')); console.log(bad.length ? 'ERRORS:\n' + bad.join('\n') : 'no page errors');
|
||||||
|
process.exit(bad.length ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arg = process.argv[2];
|
||||||
|
if (arg === 'smoke') await smoke();
|
||||||
|
else if (arg) {
|
||||||
|
const mod = await import(pathToFileURL(path.resolve(arg)).href);
|
||||||
|
const t = await open(mod.options || {});
|
||||||
|
let result, failed = false;
|
||||||
|
try { result = await mod.default({ ...t, characters: CHARS }); } catch (e) { failed = true; result = 'THROW ' + (e.stack || e); }
|
||||||
|
await t.close();
|
||||||
|
console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 1));
|
||||||
|
if (t.errors.length) { console.log('ERRORS:\n' + t.errors.join('\n')); failed = true; }
|
||||||
|
process.exit(failed ? 1 : 0);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue