// UI verification driven through the Chrome DevTools Protocol. // Run with Windows Node (same machine as Chrome), e.g.: // node verify-ui.cjs "C:\\Users\\...\\input.xlsx" "\\\\wsl.localhost\\Ubuntu\\...\\shots" // Chrome must be started with --remote-debugging-port=9444 (see DOC/05_검증). const fs = require('fs'); const path = require('path'); const APP_URL = process.env.APP_URL || 'http://127.0.0.1:5173/'; const CDP = process.env.CDP || 'http://127.0.0.1:9444'; const INPUT_FILE = process.argv[2]; const OUT_DIR = process.argv[3]; if (!INPUT_FILE || !OUT_DIR) throw new Error('INPUT_FILE and OUT_DIR are required'); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function main() { const targets = await (await fetch(`${CDP}/json`)).json(); const page = targets.find((t) => t.type === 'page'); if (!page) throw new Error('no page target'); const ws = new WebSocket(page.webSocketDebuggerUrl); await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; }); let seq = 0; const waiting = new Map(); // Console and exception events, collected from the first navigation onward (V10 ③). const consoleEvents = []; ws.onmessage = (ev) => { const m = JSON.parse(ev.data); if (m.id && waiting.has(m.id)) { waiting.get(m.id)(m); waiting.delete(m.id); } else if (m.method === 'Runtime.consoleAPICalled' && ['error', 'warning'].includes(m.params.type)) { const text = m.params.args.map((a) => a.value ?? a.description ?? '').join(' '); consoleEvents.push(`console.${m.params.type}: ${text}`); } else if (m.method === 'Runtime.exceptionThrown') { const d = m.params.exceptionDetails; consoleEvents.push(`exception: ${d.exception?.description || d.text}`); } else if (m.method === 'Log.entryAdded' && ['error', 'warning'].includes(m.params.entry.level)) { consoleEvents.push(`log.${m.params.entry.level}: ${m.params.entry.text} ${m.params.entry.url || ''}`.trim()); } }; const send = (method, params = {}) => new Promise((res) => { const id = ++seq; waiting.set(id, res); ws.send(JSON.stringify({ id, method, params })); }); const evalJs = async (expression) => { const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); if (r.result.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails).slice(0, 400)); return r.result.result.value; }; const shot = async (name) => { const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(OUT_DIR, name), Buffer.from(r.result.data, 'base64')); console.log(`SHOT ${name}`); }; await send('Page.enable'); await send('Runtime.enable'); await send('Log.enable'); await send('DOM.enable'); await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false }); await send('Page.navigate', { url: APP_URL }); await sleep(4000); // V11 — dialog with the user's three-sheet workbook. await evalJs(`document.getElementById('open-import').click()`); await sleep(500); await evalJs(`(() => { const i = document.getElementById('project-name'); i.value = '운곡교 검증'; i.dispatchEvent(new Event('input')); return true; })()`); const doc = await send('DOM.getDocument', { depth: -1 }); const q = await send('DOM.querySelector', { nodeId: doc.result.root.nodeId, selector: '#file' }); await send('DOM.setFileInputFiles', { nodeId: q.result.nodeId, files: [INPUT_FILE] }); await sleep(3000); console.log('SHEETS_TEXT_BEGIN'); console.log(await evalJs(`document.getElementById('sheets').innerText`)); console.log('SHEETS_TEXT_END'); console.log('SELECTED', await evalJs(`document.getElementById('dlg-selected').textContent`)); console.log('APPLY', await evalJs(`(() => { const b = document.getElementById('dlg-apply'); return b.textContent + ' | disabled=' + b.disabled; })()`)); await shot('v11_dialog_user_file.png'); await evalJs(`document.getElementById('dlg-apply').click()`); await sleep(2000); console.log('STATUS', await evalJs(`document.getElementById('status').textContent`)); console.log('PIERS', await evalJs(`[...document.querySelectorAll('#pier-items li')].map(li => li.textContent.trim()).join(' | ')`)); console.log('RESULT_AXIS', await evalJs(`document.getElementById('res-axis').innerText.replace(/\\n+/g, ' / ')`)); console.log('RESULT_FOUND', await evalJs(`document.getElementById('res-found').innerText.replace(/\\n+/g, ' / ')`)); await shot('v11_main_user_file.png'); // V10 ② — the four view presets. for (const label of ['정면', '측면', '평면', '아이소']) { await evalJs(`[...document.querySelectorAll('#views button')].find((b) => b.textContent === '${label}').click()`); await sleep(1500); await shot(`v10_view_${label}.png`); console.log('PRESSED', label, await evalJs(`[...document.querySelectorAll('#views button')].filter((b) => b.getAttribute('aria-pressed') === 'true').map((b) => b.textContent).join(',')`)); } // V10 ③ — every console error/warning and uncaught exception seen during the run above. console.log(`CONSOLE_EVENTS ${consoleEvents.length}`); for (const e of consoleEvents) console.log(` ${e}`); ws.close(); } main().catch((e) => { console.error('FAILED', e); process.exit(1); });