Files

131 lines
5.8 KiB
JavaScript
Raw Permalink Normal View History

2026-09-27 00:31:47 +08:00
// 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);
})();