// Run with Windows Node and an isolated Chrome on CDP 9444; Vite must be on 5173. const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); const out = path.resolve(__dirname, '../.verify/ground-abutments-20261109'); fs.mkdirSync(out, { recursive: true }); const report = { checks: [], errors: [] }; let ws, send; const delay = ms => new Promise(r => setTimeout(r, ms)); (async () => { const target = await (await fetch('http://127.0.0.1:9444/json/new?about:blank', { method: 'PUT' })).json(); ws = new WebSocket(target.webSocketDebuggerUrl); await new Promise((r, j) => { ws.onopen = r; ws.onerror = j; }); let seq = 0; const pending = new Map(); ws.onmessage = ({ data }) => { const m = JSON.parse(data), p = pending.get(m.id); if (p) { clearTimeout(p.timer); pending.delete(m.id); m.error ? p.reject(new Error(JSON.stringify(m.error))) : p.resolve(m.result); } if (m.method === 'Runtime.exceptionThrown') report.errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text); }; send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq, timer = setTimeout(() => reject(new Error('Timeout ' + method)), 30000); pending.set(id, { resolve, reject, timer }); ws.send(JSON.stringify({ id, method, params })); }); const ev = async expression => { const r = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text); return r.result.value; }; const until = async expression => { for (let i = 0; i < 120; i++) { if (await ev(expression)) return; await delay(150); } throw new Error('Wait ' + expression); }; const check = (name, value) => { assert.ok(value, name); report.checks.push(name); console.log('PASS', name); }; const click = expression => ev(`(()=>{const e=${expression};if(!e||e.disabled)throw new Error('Missing target');e.click();})()`); const shot = async name => { const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64')); }; 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: process.env.PIERZAINER_URL || 'http://127.0.0.1:5173/' }); await until(`typeof window.openSampleProject==='function'`); const selectFile=async(selector,files)=>{ const doc=await send('DOM.getDocument'),{nodeId}=await send('DOM.querySelector',{nodeId:doc.root.nodeId,selector}); await send('DOM.setFileInputFiles',{nodeId,files}); }; const folder='C:\\Users\\User\\Downloads\\NAVER WORKS'; await ev(`window.openSampleProject()`); await selectFile('#file',[path.join(folder,'옥성교 데이터_261106.xlsx')]); await until(`document.getElementById('file-name').textContent.includes('선택된 파일: 옥성교 데이터_261106.xlsx')`); await ev(`(()=>{const e=document.getElementById('project-name');e.value='교대 지반 검증';e.dispatchEvent(new Event('input'));document.getElementById('import-dialog').showModal();})()`); await click(`document.getElementById('dlg-apply')`); await click(`document.getElementById('nav-shape')`); await click(`document.getElementById('project-ground-open')`); check('manager lists six piers and both abutments before model import',await ev(`document.querySelectorAll('[data-ground-pier]').length===8&&document.querySelectorAll('[data-ground-kind="교대"]').length===2`)); await selectFile('#ground-import',[path.join(folder,'옥성교 지형.json')]); await until(`document.getElementById('project-ground-status').textContent.includes('8/8')`); check('both abutments show IP2 grid source and finite comparison elevations',await ev(`[...document.querySelectorAll('[data-ground-kind="교대"]')].every(e=>e.closest('tr').textContent.includes('IP2 격자 보간')&&!e.disabled)`)); const values=()=>ev(`[...document.querySelectorAll('.project-ground-table tbody tr')].map(r=>({name:r.children[1].textContent,el:Number(r.children[2].textContent),model:Number(r.children[3].textContent)}))`); const before=await values(); check('import alone preserves original abutment inputs',before.find(r=>r.name==='A1').el===11.24&&before.find(r=>r.name==='A2').el===14.43); await click(`document.querySelector('[data-ground-pier="A1"]')`); await click(`document.getElementById('project-ground-apply')`); const one=await values(); check('only selected A1 changes; A2 and P1 remain unchanged',one.find(r=>r.name==='A1').el===11.789&&one.find(r=>r.name==='A2').el===14.43&&one.find(r=>r.name==='P1').el===before.find(r=>r.name==='P1').el); await click(`document.getElementById('project-ground-select-all')`); check('select all includes both types',await ev(`document.querySelectorAll('[data-ground-pier]:checked').length===8`)); await click(`document.getElementById('project-ground-apply')`); const applied=await values(); check('A2 receives IP2 interpolation',applied.find(r=>r.name==='A2').el===14.61); check('pier regeneration remains explicit',await ev(`!document.getElementById('project-ground-regenerate').disabled`)); await click(`document.getElementById('project-ground-regenerate')`); check('pending pier changes regenerate',await ev(`document.getElementById('project-ground-regenerate').disabled`)); await click(`document.getElementById('project-ground-el')`); await click(`document.getElementById('project-ground-layers')`); check('missing abutment soil layers cannot be applied',await ev(`[...document.querySelectorAll('[data-ground-kind="교대"]')].every(e=>e.disabled)`)); await click(`document.getElementById('project-ground-el')`); await click(`document.getElementById('project-ground-layers')`); await shot('01-unified-ground'); await click(`document.querySelector('#project-ground-dialog .viewer-head button')`); await shot('02-shared-terrain'); await ev(`(()=>{const original=URL.createObjectURL;window.__saved=null;URL.createObjectURL=function(blob){blob.text().then(text=>{try{window.__saved=JSON.parse(text);}catch{}});return original.call(this,blob);};})()`); await send('Browser.setDownloadBehavior',{behavior:'deny'}); await click(`document.getElementById('save-project')`); await until(`document.getElementById('save-dialog').open`); await click(`document.getElementById('sv-save')`); await until(`!!window.__saved?.abutments`); const saved=await ev(`window.__saved`); check('saved project retains both abutment elevations and Excel source',Math.abs(saved.abutments.find(a=>a.name==='A1').groundEl-11.7886759049651)<1e-6&&Math.abs(saved.abutments.find(a=>a.name==='A2').groundEl-14.610314099767017)<1e-6&&saved.source.fileName==='옥성교 데이터_261106.xlsx'); const savedPath=path.join(out,'saved.pzr.json');fs.writeFileSync(savedPath,JSON.stringify(saved)); await selectFile('#project-file',[savedPath]); await click(`document.getElementById('project-ground-open')`); check('reopening project restores abutment inputs', (await values()).filter(r=>r.name==='A1'||r.name==='A2').every(r=>Math.abs(r.el-r.model)<0.001)); check('no runtime exceptions',report.errors.length===0);report.result='PASS'; })().catch(e=>{report.result='FAIL';report.failure=e.stack;process.exitCode=1;}).finally(()=>{ fs.writeFileSync(path.join(out,'report.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report));ws?.close(); });