// 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;idocument.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;});