// Proves the PixiJS topology actually DRAWS, not merely that it initialises. // // Three earlier test designs were wrong, and every one of them reported a false // failure against a perfectly working graph. They are documented here so nobody // reintroduces them: // // 1. "canvas exists and has N scene children" — passes on a fully blank canvas. // 2. "count pixels that differ from the background colour" with a tight // threshold — the theme is dark by design (node fill 0x0e1526 on bg // 0x0a0f1c, team discs at alpha 0.13), so a perfect render still measured // ~0% and looked like a total failure. // 3. Diffing two Playwright screenshots — both captures can be taken after the // scene was mutated, and the WebGL back buffer is not guaranteed to be // captured, so the diff was 0 for a graph that was plainly rendering. // // The reliable check reads the GL back buffer via readPixels INSIDE a single // page.evaluate, so the frame being measured is the frame just rendered, with no // compositor or screenshot timing involved. const { chromium } = require('/tmp/gltest/node_modules/playwright-core'); const BASE = process.env.PANEL_URL || 'https://panel.attackdefense.imrnes.team'; (async () => { const browser = await chromium.launch({ args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-unsafe-swiftshader'], }); const page = await browser.newPage({ viewport: { width: 1400, height: 900 } }); const errors = []; page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); page.on('console', m => { if (m.type() === 'error') errors.push('CONSOLE: ' + m.text()); }); const creds = require('fs').readFileSync('/opt/gemastik18-final/panel/.env', 'utf8') .split('\n').reduce((a, l) => { const m = l.match(/^PANEL_ADMIN_(USER|PASS)=(.*)$/); if (m) a[m[1]] = m[2]; return a; }, {}); await page.goto(BASE + '/login', { waitUntil: 'domcontentloaded' }); await page.fill('#u', creds.USER); await page.fill('#p', creds.PASS); await page.click('#f button[type=submit]'); await page.waitForSelector('#view-challs.active'); await page.click('.tab[data-view=topo]'); await page.waitForSelector('#topoHost canvas'); await page.waitForTimeout(3000); const state = await page.evaluate(() => { const g = window.__topoProbe(); return { nodes: g.nodeViews.size, edgeChildren: g.edgeLayer.children.length, attacks: g.attackViews.size, scale: +g.scale.toFixed(3), zoomLabel: document.getElementById('topoZoomLabel').textContent, engine: document.getElementById('topoEngine') ? document.getElementById('topoEngine').textContent : null, canvasW: g.app.renderer.width, canvasH: g.app.renderer.height, }; }); console.log('--- scene state ---'); for (const [k, v] of Object.entries(state)) console.log(` ${k}: ${v}`); // Single evaluate: render, then read the very same frame back. const pixels = await page.evaluate(() => { const g = window.__topoProbe(); const r = g.app.renderer; r.render(g.app.stage); const W = r.width, H = r.height; const buf = new Uint8Array(W * H * 4); r.gl.readPixels(0, 0, W, H, r.gl.RGBA, r.gl.UNSIGNED_BYTE, buf); // Background is 0x0a0f1c = (10, 15, 28). let nonBg = 0, bright = 0, maxDist = 0; let minX = W, minY = H, maxX = -1, maxY = -1; for (let y = 0; y < H; y++) { for (let x = 0; x < W; x++) { const i = (y * W + x) * 4; const dist = Math.max(Math.abs(buf[i] - 10), Math.abs(buf[i+1] - 15), Math.abs(buf[i+2] - 28)); if (dist > 3) { nonBg++; if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } if (dist > 60) bright++; if (dist > maxDist) maxDist = dist; } } const total = W * H; return { w: W, h: H, total, nonBg, pctNonBg: +(100 * nonBg / total).toFixed(2), bright, pctBright: +(100 * bright / total).toFixed(2), maxDist, // A blank frame has no bbox; a real graph occupies a contiguous region. bbox: maxX < 0 ? null : [minX, minY, maxX, maxY], glError: r.gl.getError(), preserveDrawingBuffer: r.preserveDrawingBuffer, }; }); console.log('\n--- GL back-buffer readback (authoritative) ---'); for (const [k, v] of Object.entries(pixels)) console.log(` ${k}: ${JSON.stringify(v)}`); // Human-visible proof for the report, plus a coverage cross-check. const box = await page.locator('#topoHost canvas').boundingBox(); const shot = await page.screenshot({ clip: box }); require('fs').writeFileSync('/tmp/topo_proof.png', shot); // The graph must ALSO be framed inside the viewport, not parked off-screen. const framed = pixels.bbox !== null && pixels.bbox[0] >= 0 && pixels.bbox[1] >= 0 && pixels.bbox[2] <= pixels.w && pixels.bbox[3] <= pixels.h; const drew = pixels.pctNonBg > 1 && pixels.maxDist > 40; const noGlError = pixels.glError === 0; console.log(`\n graph drew pixels ............. ${drew ? 'PASS' : 'FAIL'} (${pixels.pctNonBg}%, max channel delta ${pixels.maxDist})`); console.log(` graph framed in viewport ...... ${framed ? 'PASS' : 'FAIL'} (bbox ${JSON.stringify(pixels.bbox)} in ${pixels.w}x${pixels.h})`); console.log(` no GL errors ................. ${noGlError ? 'PASS' : 'FAIL'} (glGetError=${pixels.glError})`); if (state.nodes < 1) console.log(' nodes rendered ................ FAIL (0 nodes)'); else console.log(` nodes rendered ................ PASS (${state.nodes} nodes, ${state.edgeChildren} edges)`); if (errors.length) { console.log('\n page errors:'); errors.forEach(e => console.log(' ' + e)); } console.log(` screenshot -> /tmp/topo_proof.png`); const ok = drew && framed && noGlError && state.nodes > 0 && errors.length === 0; console.log(`\n ${ok ? 'PASS' : 'FAIL'}`); await browser.close(); process.exit(ok ? 0 : 1); })();