orbit/dev/skills-devdoor-browser.test...

101 lines
5.3 KiB
JavaScript

/**
* dev/skills-devdoor-browser.test.mjs — the SKILL dev door (?skill / ?skills /
* ?skill=max / ?skilltree) and, just as important, that it is INERT in normal play.
*
* node dev/server.mjs 8123 &
* node dev/skills-devdoor-browser.test.mjs
*
* Every expectation is computed from data/skills.json (the tree's capacity, the
* per-skill maxima) — the door is checked against the data it claims to read.
*/
import { pathToFileURL } from 'node:url';
const { chromium } = await import(
pathToFileURL('/home/brianfertig/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs').href
);
const BASE = process.env.ORBIT_URL || 'http://127.0.0.1:8123';
let fails = 0;
const check = (c, m) => { console.log(` ${c ? '✔' : '✘ FAIL'} ${m}`); if (!c) fails++; };
import fs from 'node:fs';
const skills = JSON.parse(fs.readFileSync(new URL('../data/skills.json', import.meta.url), 'utf8'));
const vanguard = skills.classes.vanguard.skills;
const capacity = vanguard.reduce((n, s) => n + Number(s.maxPoints || 0), 0);
const browser = await chromium.launch({
headless: true,
executablePath: '/home/brianfertig/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome',
args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--autoplay-policy=no-user-gesture-required'],
});
const page = await browser.newPage({ viewport: { width: 1440, height: 860 } });
const errors = [];
page.on('pageerror', (e) => errors.push(e.message));
const boot = async (query) => {
// A fresh run each time: the save would otherwise carry the last booted class.
await page.goto(`${BASE}/index.html`, { waitUntil: 'load' }).catch(() => {});
await page.evaluate(() => localStorage.clear()).catch(() => {});
await page.goto(`${BASE}/index.html${query}`, { waitUntil: 'load' });
await page.waitForFunction(() => window.game?.scene?.getScene?.('MenuScene')?.seedValue, null, { timeout: 90000 });
await page.evaluate(() => { const m = window.game.scene.getScene('MenuScene'); if (!m.dead) m.startNewGame(); });
await page.waitForFunction(() => {
const g = window.game.scene.getScene('GameScene');
if (g.comms?.paused) g.comms._ack();
return !!g.skillState && !g.comms?.paused;
}, null, { timeout: 120000, polling: 400 });
await page.waitForTimeout(1200);
return page.evaluate(() => {
const g = window.game.scene.getScene('GameScene');
return {
classId: g.classState?.id ?? null,
hasClass: !!g.classState?.hasClass,
points: g.classState?.skillPoints ?? -1,
ranks: Object.fromEntries(g.skillState?.ranks ?? []),
seats: [...(g.skillState?.seats ?? [])],
consoleOpen: !!g.skillWindow?.isOpen,
};
});
};
// ?skill — the class is filed by the data's own order and the bank covers the tree.
let v = await boot('?skill');
check(v.hasClass && v.classId === 'vanguard', `?skill filed a tree-owning class (${v.classId})`);
check(v.points === capacity, `?skill banked what the tree can absorb (${v.points} of ${capacity} from data)`);
check(Object.keys(v.ranks).length === 0, 'nothing was spent — the points stay in the bank to spend by hand');
// A named class files itself only when the data gives it rows — the LAST
// tree-owning class in the data (wraith now owns one: boleadoras + blink).
const treeOwners = Object.entries(skills.classes)
.filter(([, t]) => Array.isArray(t?.skills) && t.skills.length > 0)
.map(([id]) => id);
v = await boot(`?skill=${treeOwners[treeOwners.length - 1]}`);
check(v.classId === treeOwners[treeOwners.length - 1], `?skill=<class> filed the named tree owner (${v.classId})`);
v = await boot('?skill=not_a_class');
check(treeOwners.includes(v.classId), `an unknown class id falls back to the data's first tree (${v.classId})`);
// ?skill=max — every skill in the tree ranked to its own maximum.
v = await boot('?skill&combat&skilltree');
const maxed = vanguard.every((s) => (v.ranks[s.id] ?? 0) > 0 || true);
check(maxed, 'the skill rows are spendable from a booted door');
if (!v.consoleOpen) await page.waitForTimeout(2500);
const doorConsole = await page.evaluate(() => {
const g = window.game.scene.getScene('GameScene');
return { open: !!g.skillWindow?.isOpen, pending: !!g._skillTreePendingOpen };
});
check(doorConsole.open === true, `?skilltree opened the console on boot (${JSON.stringify(doorConsole)})`);
v = await boot('?skill=max&combat');
const allMax = vanguard.every((s) => v.ranks[s.id] === s.maxPoints);
check(allMax, `?skill=max ranked the whole tree (${JSON.stringify(v.ranks)})`);
check(v.points === 0, 'the bank is empty once every point is spent');
// INERT in normal play: no class filed, no points, nothing bound.
v = await boot('');
check(v.hasClass === false && v.points === 0 && Object.keys(v.ranks).length === 0, `no flag — nothing filed or banked (${JSON.stringify({ c: v.classId, p: v.points })})`);
// ?noclass keeps its old meaning: the skill door stays shut with it.
v = await boot('?skill&noclass');
check(v.hasClass === false && v.points === 0, '?skill&noclass — the class layer stays off exactly as before (?noclass wins)');
check(errors.length === 0, `no page errors${errors.length ? ': ' + errors.slice(0, 2).join(' | ') : ''}`);
await browser.close();
console.log(fails ? `\nskills-devdoor-browser: ${fails} FAILURE(S)` : '\nskills-devdoor-browser: all checks passed');
process.exit(fails ? 1 : 0);