/** * 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= 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);