From 470756127852e1578aa88c62795177780b0a9872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=84=EC=96=B8?= Date: Tue, 27 Oct 2026 16:15:45 +0900 Subject: [PATCH] test: add tests/viewer.cjs for issue #1 --- tests/viewer.cjs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/viewer.cjs diff --git a/tests/viewer.cjs b/tests/viewer.cjs new file mode 100644 index 0000000..5cfb2b5 --- /dev/null +++ b/tests/viewer.cjs @@ -0,0 +1,85 @@ +// 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=path.resolve(__dirname,'../test-results'); + +(async()=>{ + await fs.mkdir(out,{recursive:true}); + const browser=await chromium.launch({headless:true,...(process.env.PLAYWRIGHT_CHANNEL?{channel:process.env.PLAYWRIGHT_CHANNEL}:{})}); + const page=await browser.newPage({viewport:{width:1440,height:1000}}); + const errors=[];page.on('pageerror',e=>errors.push(String(e))); + let report={status:'failed',url:base}; + try{ + 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;await page.screenshot({path:path.join(out,'failure.png'),fullPage:true}).catch(()=>{});process.exitCode=1;} + finally{await fs.writeFile(path.join(out,'ui-validation.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report,null,2));await browser.close();} +})().catch(e=>{console.error(e);process.exitCode=1;});