131 lines
5.8 KiB
JavaScript
131 lines
5.8 KiB
JavaScript
// 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);
|
||
|
|
})();
|