91 lines
5.8 KiB
JavaScript
91 lines
5.8 KiB
JavaScript
// Run start.py first. VIEWER_URL defaults to http://127.0.0.1:8080.
|
||
const {chromium}=require('playwright');
|
||
const fs=require('node:fs/promises');
|
||
const path=require('node:path');
|
||
const assert=require('node:assert/strict');
|
||
const base=process.env.VIEWER_URL||'http://127.0.0.1:8080';
|
||
const out=process.env.VIEWER_RESULTS_DIR?path.resolve(process.env.VIEWER_RESULTS_DIR):path.resolve(__dirname,'../test-results');
|
||
|
||
(async()=>{
|
||
await fs.mkdir(out,{recursive:true});
|
||
let browser,page;
|
||
const errors=[];
|
||
let report={status:'failed',url:base};
|
||
try{
|
||
browser=await chromium.launch({headless:true,...(process.env.PLAYWRIGHT_CHANNEL?{channel:process.env.PLAYWRIGHT_CHANNEL}:{})});
|
||
page=await browser.newPage({viewport:{width:1440,height:1000}});
|
||
page.on('pageerror',e=>errors.push(String(e)));
|
||
await page.goto(base,{waitUntil:'networkidle'});
|
||
await page.waitForFunction(()=>document.querySelector('#loading')?.hidden && document.querySelector('#scene'));
|
||
const data=await (await page.request.get(new URL('data/network.json',base).href)).json();
|
||
assert.equal(await page.locator('#node-count').textContent(),String(data.summary.counts.junction));
|
||
assert.equal(await page.locator('#pipe-count').textContent(),String(data.summary.counts.conduit));
|
||
// Check every facility through the same selectFacility function used by UI events.
|
||
// Separate click/search checks below exercise the actual event wiring.
|
||
const inspected=await page.evaluate(async()=>{
|
||
const {app,selectFacility}=await import('./app.js');
|
||
const failures=[];
|
||
const fmt=n=>Number(n).toLocaleString('ko-KR',{minimumFractionDigits:3,maximumFractionDigits:3});
|
||
const prop=(label)=>[...document.querySelectorAll('#details .properties > div')]
|
||
.find(r=>r.querySelector('dt')?.textContent===label)?.querySelector('dd')?.textContent;
|
||
for(const f of app.facilities){
|
||
selectFacility(f);
|
||
const check=(label,expected)=>{if(prop(label)!==expected)failures.push({id:f.id,label,actual:prop(label),expected});};
|
||
if(document.querySelector('#selected-id')?.textContent!==f.id)failures.push({id:f.id,label:'selected-id'});
|
||
if(['junction','outfall','storage'].includes(f.type)){
|
||
check('바닥 표고',`${fmt(f.invert)} m`);
|
||
check('원본 X / Y',f.xy?`${fmt(f.xy[0])}${fmt(f.xy[1])}`:'좌표 없음 · 미배치');
|
||
if(f.maxDepth!=null){check('최대 깊이',`${fmt(f.maxDepth)} m`);check('모델상 상단',`${fmt(f.top)} m`);}
|
||
}else if(f.type==='conduit'){
|
||
check('입력 길이',`${fmt(f.length)} m`);
|
||
check('시작 관저고',`${fmt(f.invertStart)} m`);check('도착 관저고',`${fmt(f.invertEnd)} m`);
|
||
check('단면',f.section.shape);
|
||
if(f.section.shape==='CIRCULAR')check('안지름',`Ø ${fmt(f.section.diameter)} m`);
|
||
else check('폭 × 높이',`${fmt(f.section.width)} × ${fmt(f.section.height)} m`);
|
||
check('병렬 관 수',String(f.section.barrels));
|
||
}
|
||
const placed=f.xy!=null||f.renderable;
|
||
if(document.querySelector('#focus-selected').disabled===Boolean(placed))failures.push({id:f.id,label:'placement button'});
|
||
}
|
||
return {count:app.facilities.length,failures};
|
||
});
|
||
assert.deepEqual(inspected.failures,[]);
|
||
const samples=[data.nodes.find(n=>n.type==='junction'&&n.xy),
|
||
data.links.find(l=>l.type==='conduit'&&l.renderable&&l.section.shape==='CIRCULAR'),
|
||
data.links.find(l=>l.type==='conduit'&&l.renderable&&l.section.shape==='RECT_CLOSED'),
|
||
...data.nodes.filter(n=>!n.xy),...data.links.filter(l=>!l.renderable)];
|
||
for(let i=0;i<samples.length;i++){
|
||
const f=samples[i];
|
||
await page.locator('#search').fill(f.id);
|
||
const result=page.locator('#search-results button').filter({has:page.locator('span',{hasText:new RegExp('^'+f.id.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+'$')})});
|
||
await result.click();
|
||
assert.equal(await page.locator('#selected-id').textContent(),f.id);
|
||
if(i<3)await page.screenshot({path:path.join(out,`facility-${i}.png`),fullPage:true});
|
||
}
|
||
await page.locator('#search').fill('__NO_SUCH_FACILITY__');
|
||
assert.equal(await page.locator('#search-results button').count(),0);
|
||
await page.locator('#actual-scale').click();
|
||
assert.equal(await page.locator('#height-scale').inputValue(),'1');
|
||
assert.equal(await page.locator('#size-scale').inputValue(),'1');
|
||
await page.locator('[data-view="plan"]').click();
|
||
await page.waitForFunction(()=>document.querySelector('[data-view="plan"]').getAttribute('aria-pressed')==='true');
|
||
await page.screenshot({path:path.join(out,'plan.png'),fullPage:true});
|
||
for(const box of await page.locator('[data-layer]').all()){await box.uncheck();await box.check();}
|
||
await page.locator('#open-audit').click();
|
||
for(const id of [...data.nodes.filter(n=>!n.xy),...data.links.filter(l=>!l.renderable)].map(f=>f.id))
|
||
assert.ok((await page.locator('#audit-list').textContent()).includes(id),`audit missing ${id}`);
|
||
await page.locator('#audit-dialog .close-dialog').click();
|
||
await page.locator('#reset').click();
|
||
await page.screenshot({path:path.join(out,'overview.png'),fullPage:true});
|
||
assert.deepEqual(errors,[]);
|
||
report={status:'passed',facilities_checked:inspected.count,search_click_samples:samples.map(f=>f.id),
|
||
checks:['all facility information panels','summary counts','real search/selection clicks','missing-facility explanations',
|
||
'unknown ID','scale reset','plan view','layer toggles','audit dialog','reset','no page errors'],
|
||
browser:browser.version(),limitations:['Information consistency; screenshot review is separate from dimensional geometry verification.']};
|
||
}catch(e){report.error=e.stack;if(page)await page.screenshot({path:path.join(out,'failure.png'),fullPage:true}).catch(()=>{});process.exitCode=1;}
|
||
finally{
|
||
try{await fs.writeFile(path.join(out,'ui-validation.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report,null,2));}
|
||
finally{if(browser)await browser.close();}
|
||
}
|
||
})().catch(e=>{console.error(e);process.exitCode=1;});
|