// Run with Windows Node and an isolated Chrome on CDP 9444; Vite must be on 5173. const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'); const preview=process.argv.includes('--horizontal-preview'); const lowerPreview=process.argv.includes('--lower-preview'); const out = path.resolve(__dirname, lowerPreview?'../.verify/landing-lower-20261109':process.argv.includes('--all')?'../.verify/landing-hero-20261109':'../.verify/landing-horizontal-20261109'); fs.mkdirSync(out, { recursive: true }); const report = { checks: [], errors: [] }; let ws, send; const delay = ms => new Promise(r => setTimeout(r, ms)); (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; const 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); }; send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++seq, timer = setTimeout(() => 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 check = (name, value) => { assert.ok(value, name); report.checks.push(name); console.log('PASS', name); }; const click = expression => ev(`(()=>{const e=${expression};if(!e||e.disabled)throw new Error('Missing target');e.click();})()`); const shot = async name => { const r = await send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(path.join(out, name + '.png'), Buffer.from(r.data, 'base64')); }; await send('Page.enable'); await send('Runtime.enable'); await send('DOM.enable'); await send('Emulation.setDeviceMetricsOverride', { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false }); await send('Page.navigate', { url: process.env.PIERZAINER_URL || 'http://127.0.0.1:5173/' }); await until(`typeof window.openSampleProject==='function'`); const projectPath=process.env.OKSEONG_PROJECT||path.resolve(__dirname,'../.verify/ground-abutments-20261109/saved.pzr.json'); const projectText=fs.readFileSync(projectPath,'utf8'); const hash=require('node:crypto').createHash('sha256').update(projectText).digest('hex'); await ev(`window.__heroProject=JSON.parse(${JSON.stringify(projectText)})`); const variants=lowerPreview ? [['lower-33',.65,0,30,true,true],['lower-27',.5,0,30,true,true],['lower-19',.35,0,30,true,true],['lower-27-cutaway',.5,0,30,true,true,8]] : preview ? [['horizontal',.85,0,30,true],['horizontal-smooth',.85,0,30,true,true]] : process.argv.includes('--all') ? [['low',.35,0],['balanced',.55,.12],['high',.8,-.12],['trim30',.55,.12,30],['trim20',.55,.12,20],['refined',.85,.12,30,true]] : [['horizontal-smooth',.85,0,30,true,true]]; for(const [name,elevation,along,margin,refined,smooth,cutaway] of variants){ const result=await ev(`(async()=>{const {renderLandingHero}=await import('/scripts/render-landing-hero.ts');return renderLandingHero(window.__heroProject,${elevation},${along},${margin},${refined},${smooth},${cutaway});})()`); for(const type of ['png','webp'])fs.writeFileSync(path.join(out,name+'.'+type),Buffer.from(result[type].split(',')[1],'base64')); fs.writeFileSync(path.join(out,name+'.json'),JSON.stringify({...result.metadata,sourceSha256:hash},null,2)); check(name+' includes six piers and two abutments',result.metadata.piers===6&&result.metadata.abutments===2&&result.metadata.points.length===8); check(name+' all projected markers stay inside image',result.metadata.points.every(p=>p.x>0&&p.x0&&p.y100&&result.metadata.terrainFlow.every(p=>!p||Math.abs(p.world[1]-p.el-.12)<.0001)); if(refined)check('both road extensions contain continuous terrain-supported sections',result.metadata.roads.length===2&&result.metadata.roads.every(r=>r.missingSections===0&&r.validSections===81&&r.triangles>0)); if(smooth){ check('outline trims real pixels without filling missing terrain',result.metadata.outline.addedOutsideSourcePixels===0&&result.metadata.outline.retainedTerrainRatio>.8&&result.metadata.outline.retainedTerrainRatio<.99); check('smooth outline preserves all eight structure tops',await ev(`(()=>{const c=document.createElement('canvas');c.width=${result.metadata.width*2};c.height=${result.metadata.height*2};return new Promise(resolve=>{const i=new Image();i.onload=()=>{const ctx=c.getContext('2d');ctx.drawImage(i,0,0);resolve(${JSON.stringify(result.metadata.points)}.every(p=>{let n=0;for(let x=-8;x<=8;x++)for(let y=0;y<30;y++)n+=ctx.getImageData(Math.round(p.x*2+x),Math.round(p.y*2+y),1,1).data[3]>0;return n>15;}));};i.src=${JSON.stringify(result.png)};});})()`)); } } check('no runtime exceptions',report.errors.length===0);report.result='PASS'; })().catch(e=>{report.result='FAIL';report.failure=e.stack;process.exitCode=1;}).finally(()=>{ fs.writeFileSync(path.join(out,'report.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report));ws?.close(); });