// Headless boot check for the OA replica: load key routes, capture runtime // exceptions + console errors, confirm #app renders content. No screenshots. import { spawn } from 'node:child_process' import { mkdtemp, readFile, rm } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { setTimeout as delay } from 'node:timers/promises' const CHROME = process.env.CHROME_BIN || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' const BASE = process.env.OA_BASE || 'http://localhost:8080/modern/app/' const ROUTES = [ '#/oa', '#/oa/contacts', '#/oa/collab/todo', '#/oa/collab/create', '#/oa/collab/done', '#/oa/collab/center', '#/oa/meeting/mine', '#/oa/meeting/room', '#/oa/goal/project', '#/oa/goal/schedule', '#/oa/knowledge/doccenter', '#/oa/knowledge/portal', '#/oa/doccollab/list', '#/oa/culture/notice', '#/oa/culture/survey', '#/oa/hr/org', '#/oa/hr/staff', '#/oa/report/manage', '#/oa/appdev/workbench' ] const profile = await mkdtemp(path.join(os.tmpdir(), 'oacheck.')) const chrome = spawn(CHROME, ['--headless=new', '--disable-gpu', '--no-first-run', '--remote-debugging-port=0', '--window-size=1440,900', `--user-data-dir=${profile}`, 'about:blank'], { stdio: ['ignore', 'ignore', 'pipe'] }) let port = '' const dl = Date.now() + 15000 while (Date.now() < dl && !port) { try { port = (await readFile(path.join(profile, 'DevToolsActivePort'), 'utf8')).trim().split('\n')[0] } catch { await delay(150) } } const targets = await fetch(`http://127.0.0.1:${port}/json/list`).then((r) => r.json()) const ws = new WebSocket(targets.find((t) => t.type === 'page' && t.webSocketDebuggerUrl).webSocketDebuggerUrl) await new Promise((res) => ws.addEventListener('open', res, { once: true })) let id = 0 const pending = new Map() let errors = [] ws.addEventListener('message', (e) => { const m = JSON.parse(e.data) if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id) } else if (m.method === 'Runtime.exceptionThrown') { errors.push('EXC: ' + (m.params?.exceptionDetails?.exception?.description || m.params?.exceptionDetails?.text || 'exception').split('\n')[0]) } else if (m.method === 'Runtime.consoleAPICalled' && m.params?.type === 'error') { errors.push('ERR: ' + (m.params.args || []).map((a) => a.value || a.description || '').join(' ').slice(0, 200)) } }) const cmd = (method, params = {}) => new Promise((res) => { const i = ++id; pending.set(i, res); ws.send(JSON.stringify({ id: i, method, params })) }) const evalJs = async (expr) => (await cmd('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true })).result?.value await cmd('Page.enable'); await cmd('Runtime.enable') const results = [] for (const route of ROUTES) { errors = [] await cmd('Page.navigate', { url: BASE + route }) await delay(1700) const info = await evalJs(`(() => { const app = document.querySelector('#app'); const t = app ? app.innerText : ''; return { len: t.trim().length, head: t.trim().slice(0,40).replace(/\\s+/g,' '), shell: !!document.querySelector('.oa-shell, .oa-app-shell, [class*=oa-shell], header, nav') }; })()`).catch((e) => ({ len: 0, head: 'EVAL-FAIL ' + e.message })) results.push({ route, len: info?.len ?? 0, head: info?.head, errs: errors.slice(0, 3) }) } let ok = 0 for (const r of results) { const good = r.len > 80 && r.errs.length === 0 if (good) ok++ console.log(`${good ? 'OK ' : 'XX '} ${r.route.padEnd(28)} len=${String(r.len).padStart(5)} ${r.errs.length ? '['+r.errs.join(' | ')+']' : r.head}`) } console.log(`\nBOOTCHECK: ${ok}/${results.length} routes rendered cleanly`) ws.close(); chrome.kill('SIGTERM'); await delay(300); await rm(profile, { recursive: true, force: true }) process.exit(0)