- 현장타설 기둥·말뚝 형상(cast-in-place-shape)과 주철근·횡철근·커플러 계산(rebar/cast-in-place, cast-layout) - 다주형 코핑 철근(multicol-coping), 기둥 높이 · 양단부 연장 - 철근표 요약 · 재료 수량 · 3D 뷰 연결과 삽도(cast-rebar-svg) - 검사 기준 갱신과 회귀 테스트, 검증 스크립트 보관 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
120 lines
9.9 KiB
JavaScript
120 lines
9.9 KiB
JavaScript
// 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');
|
|
await click(`document.getElementById('nav-quantity')`);
|
|
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();
|
|
});
|