// Capture the OA form/flow engine in action: 新建事项 dialog → 预览 (form) → 流程 (flow) → 表单设计器. import { spawn } from 'node:child_process' import { mkdtemp, readFile, rm, writeFile, mkdir } 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 OUT = '/Users/qiu/Desktop/ERP/ofbiz-framework/plugins/modern-ui/verification/oa-shots' await mkdir(OUT, { recursive: true }) const profile = await mkdtemp(path.join(os.tmpdir(), 'oaeng.')) 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() 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) } }) 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) => { const r = await cmd('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); return r.result?.value } const shot = async (name) => { const s = await cmd('Page.captureScreenshot', { format: 'png' }); if (s?.data) { await writeFile(path.join(OUT, name + '.png'), Buffer.from(s.data, 'base64')); console.log('saved', name) } } // click the first element whose visible text contains `txt` (prefers buttons/clickable) const clickByText = async (txt) => evalJs(`(() => { const t = ${JSON.stringify(txt)}; const els = Array.from(document.querySelectorAll('button, a, .el-button, [role=button], .el-tabs__item, li, .tpl-card, .template-card, span, div')); const hit = els.reverse().find(e => (e.innerText||e.textContent||'').trim().includes(t) && e.offsetParent !== null); if (hit) { (hit.closest('button,a,.el-button,.el-tabs__item') || hit).click(); return true; } // also try hidden-but-present elements (hover-revealed buttons) const any = els.find(e => (e.innerText||e.textContent||'').trim() === t); if (any) { any.click(); return true; } return false; })()`) await cmd('Page.enable'); await cmd('Runtime.enable') // 1) 新建事项 launcher page + open dialog await cmd('Page.navigate', { url: BASE + '#/oa/collab/create' }); await delay(2000) await shot('eng-1-create-page') await clickByText('新建事项'); await delay(1500) await shot('eng-2-template-picker') // 2) preview a template (form) await clickByText('预览'); await delay(1800) await shot('eng-3-form-preview') // 3) flow tab await clickByText('流程'); await delay(1500) await shot('eng-4-flow-diagram') // close any dialogs await evalJs(`document.querySelectorAll('.el-dialog__headerbtn, .el-overlay').forEach(()=>{}); document.body.click && document.body.click()`) await cmd('Runtime.evaluate', { expression: "document.querySelectorAll('.el-dialog__headerbtn').forEach(b=>b.click())" }); await delay(800) // 4) form designer (应用管理中心) await cmd('Page.navigate', { url: BASE + '#/oa/appdev/appmgr' }); await delay(2200) await shot('eng-5-appmgr') await clickByText('新建表单'); await delay(1500) await shot('eng-6-form-designer') ws.close(); chrome.kill('SIGTERM'); await delay(300); await rm(profile, { recursive: true, force: true }) console.log('done') process.exit(0)