diff --git a/scripts/verify-ui.cjs b/scripts/verify-ui.cjs new file mode 100644 index 0000000..016351e --- /dev/null +++ b/scripts/verify-ui.cjs @@ -0,0 +1,100 @@ +// 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(); + 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); + } + }; + 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('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 ③ — console errors surfaced by the page (checked via a reload with error capture). + ws.close(); +} + +main().catch((e) => { + console.error('FAILED', e); + process.exit(1); +}); diff --git a/verify/shots-2026-10-21/v10_view_아이소.png b/verify/shots-2026-10-21/v10_view_아이소.png new file mode 100644 index 0000000..b169b28 Binary files /dev/null and b/verify/shots-2026-10-21/v10_view_아이소.png differ diff --git a/verify/shots-2026-10-21/v10_view_정면.png b/verify/shots-2026-10-21/v10_view_정면.png new file mode 100644 index 0000000..61a98a7 Binary files /dev/null and b/verify/shots-2026-10-21/v10_view_정면.png differ diff --git a/verify/shots-2026-10-21/v10_view_측면.png b/verify/shots-2026-10-21/v10_view_측면.png new file mode 100644 index 0000000..1b4b0d0 Binary files /dev/null and b/verify/shots-2026-10-21/v10_view_측면.png differ diff --git a/verify/shots-2026-10-21/v10_view_평면.png b/verify/shots-2026-10-21/v10_view_평면.png new file mode 100644 index 0000000..ba343f6 Binary files /dev/null and b/verify/shots-2026-10-21/v10_view_평면.png differ diff --git a/verify/shots-2026-10-21/v11_dialog_user_file.png b/verify/shots-2026-10-21/v11_dialog_user_file.png new file mode 100644 index 0000000..b3cdd7d Binary files /dev/null and b/verify/shots-2026-10-21/v11_dialog_user_file.png differ diff --git a/verify/shots-2026-10-21/v11_main_user_file.png b/verify/shots-2026-10-21/v11_main_user_file.png new file mode 100644 index 0000000..b169b28 Binary files /dev/null and b/verify/shots-2026-10-21/v11_main_user_file.png differ