// Actual app verification in an isolated Windows Chrome, CDP port 9444. const fs = require('node:fs'); const path = require('node:path'); const assert = require('node:assert/strict'); const out = path.resolve(__dirname, '../.verify/cast-input-20261105'); fs.mkdirSync(out, { recursive: true }); const report = { checks: [], errors: [] }; const delay = ms => new Promise(r => setTimeout(r, ms)); let ws, send; (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, download; const completed = new Set(), 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); if (m.method === 'Browser.downloadWillBegin') download = m.params.guid; if (m.method === 'Browser.downloadProgress' && m.params.state === 'completed') completed.add(m.params.guid); }; send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq, timer = setTimeout(() => { pending.delete(id); reject(new Error(`Timeout ${method}`)); }, 25000); 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 failed: ${expression}`); }; const click = expression => ev(`(() => { const e = ${expression}; if (!e || e.disabled) throw new Error('Missing/disabled target: ' + ${JSON.stringify(expression)}); e.click(); })()`); const check = (name, pass) => { assert.ok(pass, name); report.checks.push(name); console.log('PASS', name); }; const file = async name => { const doc = await send('DOM.getDocument'); const { nodeId } = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: '#project-file' }); await send('DOM.setFileInputFiles', { nodeId, files: [name] }); }; const shot = async name => { await delay(250); const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64')); }; const member = text => click(`[...document.querySelectorAll('#rebar-tabs button')].find(b => b.textContent.startsWith(${JSON.stringify(text)}))`); const select = (id, name) => click(`[...document.querySelectorAll('#${id} button')].find(b => b.textContent.startsWith(${JSON.stringify(name + ' ·')}))`); const set = (id, value) => ev(`document.getElementById(${JSON.stringify(id)}).value = ${JSON.stringify(String(value))}`); await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable'); await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false }); const downloads = 'C:\\Users\\User\\AppData\\Local\\Temp\\pierzainer-cast-input-20261105\\downloads'; await send('Browser.setDownloadBehavior', { behavior: 'allowAndName', downloadPath: downloads, eventsEnabled: true }); await send('Page.navigate', { url: 'http://127.0.0.1:5173/' }); await until(`typeof window.openSampleProject === 'function'`); const fixture = await ev(`(async () => { const XLSX = await import('/node_modules/xlsx/xlsx.mjs'); const {parseWorkbook} = await import('/src/core/parseWorkbook.ts'); const b = await (await fetch('/samples/' + encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer(); const base = parseWorkbook(XLSX.read(b)).piers.find(p=>p.name==='P1'); const piers=[]; for (const [type,columnCount] of [['T형',1],['2주형',2],['3주형',3]]) for (const foundationType of ['현장타설말뚝','직접기초','말뚝기초']) { const input={...base,name:'P'+(piers.length+1),type,columnCount,foundationType}; piers.push({id:input.name,input,defaults:input,draft:input}); } return {schemaVersion:1,project:{name:'현장타설 입력 검증',bridge:'검증',client:'',author:'',crs:'',memo:''},source:{fileName:'합성 표본',sheets:[]},piers,history:[]}; })()`); const fixturePath = path.join(out, 'fixture.pzr.json'); fs.writeFileSync(fixturePath, JSON.stringify(fixture)); await ev('window.openSampleProject()'); await click(`document.getElementById('open-project')`); await file(fixturePath); await until(`document.querySelectorAll('#plan-pier-items button').length === 9`); for (let i = 1; i <= 9; i++) { const cast = [1,4,7].includes(i); await click(`document.getElementById('nav-plan')`); await select('plan-pier-items', 'P'+i); await click(`[...document.querySelectorAll('#members button')].find(b => b.textContent.startsWith(${JSON.stringify(cast ? '현장타설말뚝' : '확대기초')}))`); check(`P${i} plan: appropriate foundation controls`, await ev(cast ? `!!document.querySelector('#members button[disabled]') && !!document.querySelector('#member-fields #edit-pileLengthMm') && !document.querySelector('#member-fields #edit-footingHeightMm')` : `!!document.querySelector('#member-fields #edit-footingHeightMm:not([disabled])')`)); await click(`document.getElementById('nav-rebar')`); await member('코핑'); check(`P${i} entering rebar builds coping drawings immediately`, await ev(`!!document.querySelector('#rebar-table figure svg')`)); await member('기둥'); check(`P${i} rebar: dedicated or legacy column`, await ev(cast ? `!!document.querySelector('[data-cast-part="column"]') && [...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.startsWith('확대기초')).disabled` : `!!document.getElementById('column-nDiv') && !document.querySelector('[data-cast-part]')`)); } await select('rebar-pier-items','P1'); await member('기둥'); await set('cip-column-mainDiameterMm',32); await set('cip-column-mainCount',46); await set('cip-column-coverMm',100); await set('cip-connection-mode','continuous'); await click(`document.getElementById('cip-column-apply')`); await until(`document.getElementById('cip-column-mainCount')?.value === '46'`); await shot('01-column-input'); await member('현장타설말뚝'); await set('cip-pile-cageLengthMm',999999); await click(`document.getElementById('cip-pile-apply')`); check('invalid cage length rejected with inline reason', await ev(`document.querySelector('[data-cast-part="pile"]').textContent.includes('철근망 길이가 말뚝 길이보다')`)); await set('cip-pile-cageLengthMm',12000); await set('cip-pile-mainCount',46); await set('cip-pile-tieKind','spiral'); await click(`document.getElementById('cip-pile-apply')`); await until(`document.getElementById('cip-pile-cageLengthMm')?.value === '12000'`); check('pile panel shares the column connection', await ev(`document.querySelector('[data-cast-part="pile"]').textContent.includes('공통 연결 방식: 연속')`)); await shot('02-pile-input'); // ④ is no longer a header step (header ①②③, 2026-11-06): open the 재료표 from ③. await click(`document.getElementById('nav-rebar')`); await click(`[...document.querySelectorAll('button')].find(b=>b.textContent==='철근재료표 보기')`); check('quantity clearly marks incomplete design', await ev(`document.getElementById('quantity-body').textContent.includes('전체 수량 미완료') && document.getElementById('quantity-body').textContent.includes('접합부')`)); await shot('03-quantity-incomplete'); await click(`document.getElementById('save-project')`); if (await ev(`document.getElementById('save-dialog').open`)) await click(`document.getElementById('sv-save')`); for (let i=0;i<120 && !(download && completed.has(download));i++) await delay(150); assert.ok(download && completed.has(download), 'project download completes'); const savedText = fs.readFileSync(path.join(downloads,download),'utf8'); const saved = JSON.parse(savedText), dedicated = saved.piers.find(p=>p.input.name==='P1').input.castInPlaceRebar; check('download preserves independent column, pile and connection', dedicated.column.mainCount===46 && dedicated.pile.cageLengthMm===12000 && dedicated.connection.mode==='continuous'); const savedPath = path.join(out,'saved.pzr.json'); fs.writeFileSync(savedPath,savedText); await send('Page.navigate',{url:'http://127.0.0.1:5173/'}); await until(`typeof window.openSampleProject === 'function'`); await ev('window.openSampleProject()'); await click(`document.getElementById('open-project')`); await file(savedPath); await until(`document.querySelectorAll('#rebar-pier-items button').length===9`); await click(`document.getElementById('nav-rebar')`); await select('rebar-pier-items','P1'); await member('현장타설말뚝'); check('fresh reopen restores pile values and shared connection', await ev(`document.getElementById('cip-pile-cageLengthMm').value==='12000' && document.querySelector('[data-cast-part="pile"]').textContent.includes('공통 연결 방식: 연속')`)); await member('기둥'); check('fresh reopen restores independent column values', await ev(`document.getElementById('cip-column-mainCount').value==='46'`)); check('no runtime exceptions in actual app',report.errors.length===0); report.result='PASS'; })().catch(e=>{ report.result='FAIL'; report.failure=e.stack; process.exitCode=1; }).finally(async()=>{ if(report.result==='FAIL' && send) try { const r=await send('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(out,'failure.png'),Buffer.from(r.data,'base64')); } catch {} fs.writeFileSync(path.join(out,'report.json'),JSON.stringify(report,null,2)); console.log(JSON.stringify({result:report.result,checks:report.checks.length,errors:report.errors,failure:report.failure})); ws?.close(); });