import http from 'node:http'; import fs from 'node:fs/promises'; import fsSync from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { chromium } from 'file:///C:/Users/toshio/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs'; const here = path.dirname(fileURLToPath(import.meta.url)); const workspace = path.resolve(here, '../../..'); const previewDir = path.resolve(here, '../preview'); const qaPath = path.resolve(here, '../qa-results.json'); const screenshotScreens = ['board','battle','gear','shop','dig','bubble','revive','victory']; const expectedScreens = ['board','dice','third-die','battle','gear','shop','tile-install','chest','campfire','smithy','blessing','dig','legendary','lotto','blackjack','claw','carriage','dice-machine','bubble','revive','victory','defeat','pause']; const mime = new Map([ ['.html','text/html; charset=utf-8'],['.css','text/css; charset=utf-8'],['.js','text/javascript; charset=utf-8'], ['.json','application/json; charset=utf-8'],['.png','image/png'],['.jpg','image/jpeg'],['.jpeg','image/jpeg'] ]); const server = http.createServer(async (req,res) => { try { const url = new URL(req.url, 'http://127.0.0.1'); const decoded = decodeURIComponent(url.pathname); const relative = decoded === '/' ? 'design/ue-ui/prototype/index.html' : decoded.replace(/^\//,''); let target = path.resolve(workspace, relative); if (!target.startsWith(workspace)) throw new Error('Path outside workspace'); const stat = await fs.stat(target); if (stat.isDirectory()) target = path.join(target, 'index.html'); const data = await fs.readFile(target); res.writeHead(200, {'Content-Type': mime.get(path.extname(target).toLowerCase()) || 'application/octet-stream','Cache-Control':'no-store'}); res.end(data); } catch { res.writeHead(404, {'Content-Type':'text/plain; charset=utf-8'}); res.end('Not found'); } }); await new Promise(resolve => server.listen(0,'127.0.0.1',resolve)); const port = server.address().port; const candidates = [ 'C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe' ]; const executablePath = candidates.find(p => fsSync.existsSync(p)); if (!executablePath) throw new Error('No local Chrome or Edge executable found.'); await fs.mkdir(previewDir,{recursive:true}); const browser = await chromium.launch({headless:true,executablePath}); const context = await browser.newContext({viewport:{width:750,height:1334},deviceScaleFactor:1}); const page = await context.newPage(); const requestFailures = []; const pageErrors = []; page.on('requestfailed',r => requestFailures.push({url:r.url(),error:r.failure()?.errorText || 'unknown'})); page.on('pageerror',e => pageErrors.push(String(e))); const results = []; try { for (const id of screenshotScreens) { await page.goto(`http://127.0.0.1:${port}/design/ue-ui/prototype/?screen=${id}&capture=1`,{waitUntil:'networkidle'}); await page.waitForFunction(() => [...document.images].every(img => img.complete)); const metrics = await page.evaluate(() => { const phone=document.querySelector('#phone'); const rect=phone.getBoundingClientRect(); const broken=[...document.images].filter(img=>!img.naturalWidth).map(img=>img.getAttribute('src')); return {width:rect.width,height:rect.height,imageCount:document.images.length,broken}; }); await page.screenshot({path:path.join(previewDir,`${id}.png`),fullPage:false}); results.push({screen:id,...metrics,screenshot:`preview/${id}.png`}); } await page.goto(`http://127.0.0.1:${port}/design/ue-ui/prototype/?screen=board&capture=1`,{waitUntil:'networkidle'}); const coverage = await page.evaluate(async ids => { const rows=[]; for (const id of ids) { window.HEROLL_UI.selectScreen(id); await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))); const root=document.querySelector('.game-screen'); const imgs=[...document.images]; rows.push({id,rendered:Boolean(root),images:imgs.length,broken:imgs.filter(i=>!i.naturalWidth).map(i=>i.getAttribute('src')),primary:document.querySelector('.primary,.result-button')?.textContent?.trim() || ''}); } return rows; }, expectedScreens); const qa = { generatedAt:new Date().toISOString(), target:{width:750,height:1334,aspect:'9:16 portrait'}, browser:{executablePath}, summary:{ expectedScreenCount:expectedScreens.length, renderedScreenCount:coverage.filter(r=>r.rendered).length, screenshotCount:results.length, brokenImageCount:[...results.flatMap(r=>r.broken),...coverage.flatMap(r=>r.broken)].length, requestFailureCount:requestFailures.length, pageErrorCount:pageErrors.length, dimensionsPass:results.every(r=>r.width===750 && r.height===1334) }, screenshots:results, coverage, requestFailures, pageErrors }; qa.summary.pass = qa.summary.renderedScreenCount===qa.summary.expectedScreenCount && qa.summary.brokenImageCount===0 && qa.summary.requestFailureCount===0 && qa.summary.pageErrorCount===0 && qa.summary.dimensionsPass; await fs.writeFile(qaPath,JSON.stringify(qa,null,2),'utf8'); console.log(JSON.stringify(qa.summary,null,2)); if (!qa.summary.pass) process.exitCode=1; } finally { await browser.close(); await new Promise(resolve=>server.close(resolve)); }