- 현장타설 기둥·말뚝 형상(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>
101 lines
12 KiB
JavaScript
101 lines
12 KiB
JavaScript
// Actual app: isolated Windows Chrome on CDP 9444, Vite on 5173. Run with Windows Node.
|
|
const fs=require('node:fs'), path=require('node:path'), assert=require('node:assert/strict');
|
|
const out=path.resolve(__dirname,process.argv[2]||'../.verify/cast-rebar-20261106'); fs.mkdirSync(out,{recursive:true});
|
|
const report={checks:[],errors:[],started:new Date().toISOString()};
|
|
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 pending=new Map(), completed=new Set();
|
|
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);
|
|
if(m.method==='Page.javascriptDialogOpening'&&m.params.type==='beforeunload')send('Page.handleJavaScriptDialog',{accept:true}).catch(e=>report.errors.push(e.message));
|
|
};
|
|
send=(method,params={})=>new Promise((resolve,reject)=>{const id=++seq,timer=setTimeout(()=>{pending.delete(id);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 click=expression=>ev(`(()=>{const e=${expression};if(!e||e.disabled)throw new Error('Missing/disabled target');e.click();})()`);
|
|
const set=(id,value)=>ev(`(()=>{const e=document.getElementById(${JSON.stringify(id)});e.value=${JSON.stringify(String(value))};e.dispatchEvent(new Event(e.tagName==='SELECT'?'change':'input',{bubbles:true}));})()`);
|
|
const check=(name,value)=>{assert.ok(value,name);report.checks.push(name);console.log('PASS',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 open=async file=>{await click(`document.querySelector('#file-menu summary')`);await click(`document.getElementById('open-project')`);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:[file]});};
|
|
const pier=name=>click(`[...document.querySelectorAll('#rebar-pier-items button')].find(b=>b.textContent.startsWith(${JSON.stringify(name+' ·')}))`);
|
|
const member=text=>click(`[...document.querySelectorAll('#rebar-tabs button')].find(b=>b.textContent.startsWith(${JSON.stringify(text)}))`);
|
|
const table=`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].filter(r=>r.cells.length===14).map(r=>({name:r.cells[2].textContent,size:r.cells[3].textContent,grade:r.cells[4].textContent,count:Number(r.cells[11].textContent)}))`;
|
|
const downloads=path.join(out,'downloads');fs.mkdirSync(downloads,{recursive:true});
|
|
await send('Page.enable');await send('Runtime.enable');await send('DOM.enable');
|
|
await send('Browser.setDownloadBehavior',{behavior:'allowAndName',downloadPath:downloads,eventsEnabled:true});
|
|
await send('Emulation.setDeviceMetricsOverride',{width:1600,height:1000,deviceScaleFactor:1,mobile:false});
|
|
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'),{parseWorkbook}=await import('/src/core/parseWorkbook.ts');
|
|
const bytes=await(await fetch('/samples/'+encodeURIComponent('운곡교P1입력.xlsx'))).arrayBuffer();
|
|
const base=parseWorkbook(XLSX.read(bytes)).piers.find(p=>p.name==='P1'),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,groundEl:30,pileLengthMm:24000,
|
|
...(foundationType==='현장타설말뚝'?{castInPlaceShape:{clayBottomEl:16},castInPlaceRebar:{column:{endZoneLengthMm:2000}}}:{})};
|
|
piers.push({id:input.name,input,defaults:input,draft:input});
|
|
}return{schemaVersion:1,project:{name:'현장타설 실배근 검증',bridge:'검증',client:'',author:'',crs:'',memo:''},source:{fileName:'9개 합성 표본',sheets:[]},piers,history:[]};
|
|
})()`);
|
|
const file=path.join(out,'fixture.pzr.json');fs.writeFileSync(file,JSON.stringify(fixture));
|
|
await ev('window.openSampleProject()');await open(file);await until(`document.querySelectorAll('#rebar-pier-items button').length===9`);
|
|
await click(`document.getElementById('nav-rebar')`);
|
|
for(let i=1;i<=9;i++){
|
|
await pier('P'+i);await member('기둥');const cast=[1,4,7].includes(i);
|
|
const rows=await ev(table);
|
|
check(`P${i}: appropriate real main and tie rows`,cast
|
|
? ['기둥-P1','기둥-P2','말뚝-P1','말뚝-P2','기둥-P3','말뚝-P3','기둥-P5','말뚝-P5','접합-P1 커플러'].every(n=>rows.some(r=>r.name===n&&r.count>0))
|
|
: ['P1','P2','P3','P5'].every(n=>rows.some(r=>r.name===n&&r.count>0)));
|
|
check(`P${i}: list reports actual support status`,await ev(`[...document.querySelectorAll('#rebar-pier-items button')].find(b=>b.textContent.startsWith('P${i} ·')).parentElement.textContent.includes('${cast||i>=4?'일부 계산':'계산됨'}')`));
|
|
}
|
|
for(const i of [4,5,6,7,8,9]){
|
|
await pier('P'+i);await member('코핑');const rows=await ev(table);
|
|
check(`P${i}: multi drawing diameters reach actual table`,[['C1','H25'],['C7','H29'],['C8','H25'],['C10','H22'],['C12','H16'],['C14','H16']].every(([name,size])=>rows.some(r=>r.name===name&&r.size===size&&r.count>0)));
|
|
check(`P${i}: multi defaults and remaining detail notice are visible`,await ev(`document.getElementById('coping-spacingS').value==='300'&&document.getElementById('coping-sideSpacing').value==='200'&&document.getElementById('coping-c14UpperSpacing').value==='600'&&document.getElementById('rebar-issues').textContent.includes('cycle별 이음')`));
|
|
}
|
|
await shot('00-multi-coping');
|
|
await pier('P1');await member('기둥');
|
|
check('dedicated drawing uses generated front and section paths',await ev(`!!document.querySelector('[data-cast-part] figure svg polyline') && document.querySelectorAll('[data-cast-part] figure svg circle').length>50`));
|
|
check('missing bottom P6 remains explicit rather than hiding all reinforcement',await ev(`document.getElementById('rebar-issues').textContent.includes('말뚝 저부 P6 검토안 미적용')`));
|
|
await set('review-model-height',420);await shot('01-real-rebar');
|
|
const baseline=await ev(table);
|
|
await set('cip-common-mainCount',50);await set('cip-common-mainDiameterMm',35.8);await set('cip-common-mainGradeMpa',400);await click(`document.getElementById('cip-apply')`);
|
|
const edited=await ev(table);
|
|
check('shared edits regenerate physical piece counts, diameters and grades',edited.filter(r=>r.name.endsWith('-P1')).every(r=>r.count%50===0&&r.count>=50&&r.size==='Ø35.8'&&r.grade==='SD400'));
|
|
check('regeneration changes actual quantities rather than just form values',JSON.stringify(edited)!==JSON.stringify(baseline));
|
|
await set('cip-pile-tieKind','spiral');await click(`document.getElementById('cip-apply')`);
|
|
check('spiral produces its own real table rows', (await ev(table)).some(r=>r.name==='말뚝-P3 나선'&&r.count>0));
|
|
await set('cip-pile-mainCount',48);await click(`document.getElementById('cip-apply')`);
|
|
check('mismatched joints remove interface couplers and report the mismatch',(await ev(table)).every(r=>!r.name.startsWith('접합-'))&&await ev(`document.getElementById('rebar-issues').textContent.includes('불일치')`));
|
|
await set('cip-pile-mainCount',50);await click(`document.getElementById('cip-apply')`);
|
|
await click(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].find(r=>r.cells[2].textContent==='말뚝-P1')`);
|
|
check('pile table selection highlights the corresponding generated mark',await ev(`[...document.querySelectorAll('#rebar-table tr.hl')].some(r=>r.cells[2].textContent==='말뚝-P1')`));
|
|
await click(`[...document.querySelectorAll('#rebar-table button')].find(b=>b.textContent==='선택만 보기')`);
|
|
await shot('02-pile-main-selected');
|
|
await click(`[...document.querySelectorAll('#rebar-table table.rebar-table tbody tr')].find(r=>r.cells[2].textContent==='말뚝-P1')`);
|
|
await click(`[...document.querySelectorAll('#rebar-table button')].find(b=>b.textContent==='선택만 보기')`);
|
|
await ev(`(()=>{const d=[...document.querySelectorAll('[data-cast-part] details')].find(d=>d.querySelector('summary').textContent.includes('삽도'));d.open=true;d.scrollIntoView({block:'start'});})()`);await shot('03-drawing');
|
|
for(const width of [1280,1600,1920]){
|
|
await send('Emulation.setDeviceMetricsOverride',{width,height:1000,deviceScaleFactor:1,mobile:false});await delay(150);
|
|
check(`header ${width} keeps one row and no horizontal overflow`,await ev(`document.querySelector('.top').getBoundingClientRect().height<=65&&document.documentElement.scrollWidth<=innerWidth`));
|
|
}
|
|
await click(`document.getElementById('nav-quantity')`);
|
|
check('quantity contains actual dedicated mains and reports remaining incomplete details',await ev(`document.getElementById('quantity-body').textContent.includes('말뚝-P1')&&document.getElementById('quantity-body').textContent.includes('기둥-P1')&&document.getElementById('quantity-body').textContent.includes('전체 수량 미완료')`));
|
|
await shot('04-quantity');
|
|
await click(`document.getElementById('nav-rebar')`);await member('기둥');const beforeSave=await ev(table);
|
|
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),'download completes');
|
|
const text=fs.readFileSync(path.join(downloads,download),'utf8'),savedPath=path.join(out,'saved.pzr.json');fs.writeFileSync(savedPath,text);
|
|
const saved=JSON.parse(text).piers.find(p=>p.input.name==='P1').input.castInPlaceRebar;
|
|
check('download preserves generating inputs',saved.column.mainCount===50&&saved.pile.mainCount===50&&saved.pile.tieKind==='spiral'&&saved.column.mainGradeMpa===400);
|
|
await send('Page.navigate',{url:'http://127.0.0.1:5173/'});await until(`typeof window.openSampleProject==='function'`);await ev('window.openSampleProject()');await open(savedPath);await until(`document.querySelectorAll('#rebar-pier-items button').length===9`);
|
|
await click(`document.getElementById('nav-rebar')`);await pier('P1');await member('기둥');
|
|
check('fresh-page restore regenerates identical physical bar rows',JSON.stringify(await ev(table))===JSON.stringify(beforeSave));
|
|
check('no runtime exceptions',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,failure:report.failure}));ws?.close();
|
|
});
|