Replaces the hand-rolled inline-SVG topology with a PixiJS 8 scene graph. Why: the old renderer rebuilt all 37 nodes / 36 edges as one innerHTML string every 10s, which tore down and recreated every DOM node. That restarted CSS animations mid-flight and made dragging fight the browser's own hit-testing. The scene graph gives per-node transforms, so pan/zoom is a single container transform instead of getScreenCTM() matrix math. Changes: - static/topo_pixi.js: new self-contained renderer. Owns its Application and tears it down on tab exit so a second WebGL context cannot leak. - static/index.html: the <svg id=topoSvg> host becomes a <div id=topoHost>; the 188-line SVG renderer is replaced by a bridge to the module. - static/vendor/pixi.mjs: PixiJS 8.21.0 self-hosted (MIT). The .mjs build is required; the .js build exports no global. See vendor/README.md. - main.py: mount /static. Pages were served as inline HTMLResponse, so the directory was never mounted and the module had no URL to load from. Two real bugs found by measuring pixels rather than trusting init(): - preserveDrawingBuffer: without it WebGL clears the back buffer after compositing, so any readback or screenshot of the canvas is a coin flip depending on which frame it lands on. The graph rendered intermittently blank. Now enabled: cheap for a 2D scene, and it makes the view capturable. - Layout was centred on the SCROLLABLE width (nodes.length * 130), not the viewport, so with 37 nodes every team and challenge node landed at x=2230-2650 on a 1310px canvas: entirely off-screen. Layout now centres on the visible width and reset() frames the whole graph to fit. test_topo_pixels.js documents three wrong test designs it replaces, all of which reported false failures against a working graph: counting scene-graph children (passes on a blank canvas), diffing against the background colour (the theme is dark by design, so a perfect render measures ~0%), and diffing two Playwright screenshots (both can be captured after the scene was mutated). The check now reads the GL back buffer via readPixels in one evaluate. Verified: 37 nodes / 36 edges drawn (7.94% of frame, max channel delta 225), graph bbox [437,46,881,476] inside the 1310x520 canvas, glGetError=0, no page errors, zoom and frame-to-fit reset working. Platform unregressed: SLA 32/32.
98 lines
4.8 KiB
JavaScript
98 lines
4.8 KiB
JavaScript
// Real-browser test of the PixiJS topology: boot the panel, log in, open the
|
|
// Topology tab, and assert a live WebGL canvas actually rendered the graph.
|
|
// A JS-parse check cannot catch "init() rejected" or "canvas sized to 0".
|
|
const { chromium } = require('/tmp/gltest/node_modules/playwright-core');
|
|
|
|
(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 = [], logs = [];
|
|
page.on('pageerror', e => errors.push('pageerror: ' + e.message));
|
|
page.on('console', m => { if (m.type() === 'error') errors.push('console: ' + m.text()); else logs.push(m.text()); });
|
|
page.on('requestfailed', r => errors.push(`requestfailed: ${r.url()} ${r.failure()?.errorText}`));
|
|
|
|
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('https://panel.attackdefense.imrnes.team/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', { timeout: 20000 });
|
|
|
|
// Open the topology tab.
|
|
await page.click('.tab[data-view=topo]');
|
|
await page.waitForSelector('#topoHost canvas', { timeout: 30000 });
|
|
await page.waitForTimeout(4000);
|
|
|
|
const res = await page.evaluate(async () => {
|
|
// The inline script's top-level `let topoGraph` is not a window property,
|
|
// so read it through the page's deliberate debug hook.
|
|
const g = window.__topoProbe ? window.__topoProbe() : null;
|
|
const host = document.getElementById('topoHost');
|
|
const cv = host && host.querySelector('canvas');
|
|
return {
|
|
hasCanvas: !!cv,
|
|
canvasW: cv ? cv.width : 0,
|
|
canvasH: cv ? cv.height : 0,
|
|
cssW: cv ? cv.getBoundingClientRect().width : 0,
|
|
cssH: cv ? cv.getBoundingClientRect().height : 0,
|
|
pixiLoaded: !!(g && g.PIXI),
|
|
nodeViews: g ? g.nodeViews.size : 0,
|
|
attackViews: g ? g.attackViews.size : 0,
|
|
edgeChildren: g && g.edgeLayer ? g.edgeLayer.children.length : 0,
|
|
nodeChildren: g && g.nodeLayer ? g.nodeLayer.children.length : 0,
|
|
scale: g ? g.scale : null,
|
|
engine: document.getElementById('topoEngine')?.textContent || '',
|
|
// Are pixels actually drawn, or an empty background?
|
|
zoomLabel: document.getElementById('topoZoomLabel')?.textContent,
|
|
};
|
|
});
|
|
|
|
console.log('--- topology render state ---');
|
|
for (const [k, v] of Object.entries(res)) console.log(` ${k}: ${v}`);
|
|
|
|
// Zoom controls must reach the Pixi world transform.
|
|
await page.click('#view-topo button:has-text("Zoom In")');
|
|
await page.waitForTimeout(400);
|
|
const zoomed = await page.evaluate(() => ({ scale: window.__topoProbe().scale, label: document.getElementById('topoZoomLabel').textContent }));
|
|
console.log(' after zoom-in ->', JSON.stringify(zoomed));
|
|
await page.click('#view-topo button:has-text("\u27f3")');
|
|
await page.waitForTimeout(300);
|
|
// Reset now means "frame the whole graph in the viewport", not "scale=1": with
|
|
// 37 nodes the graph is wider than the canvas, so the fit scale is < 1. Assert
|
|
// the real contract — that reset returns to the framed scale and the view
|
|
// transform is centred — rather than a magic 1 that only held for small graphs.
|
|
const before = await page.evaluate(() => window.__topoProbe().scale);
|
|
await page.click('#view-topo button:has-text("\u27f3")');
|
|
await page.waitForTimeout(400);
|
|
const after = await page.evaluate(() => ({
|
|
scale: window.__topoProbe().scale,
|
|
panX: window.__topoProbe().panX,
|
|
label: document.getElementById('topoZoomLabel').textContent,
|
|
}));
|
|
console.log(' after zoom -> scale', before.toFixed(3));
|
|
console.log(' after reset -> scale', after.scale.toFixed(3), 'label', after.label);
|
|
if (Math.abs(after.scale - before) > 0.001) {
|
|
console.log(` FAIL: reset did not restore the framed scale (${after.scale} vs ${before})`);
|
|
process.exitCode = 1;
|
|
} else {
|
|
console.log(' PASS: reset restored the framed scale');
|
|
}
|
|
|
|
await page.screenshot({ path: '/tmp/topo_pixi.png' });
|
|
console.log(' screenshot -> /tmp/topo_pixi.png');
|
|
|
|
if (errors.length) { console.log('\n--- ERRORS ---'); errors.slice(0, 12).forEach(e => console.log(' ' + e)); }
|
|
else console.log('\n no page errors');
|
|
|
|
const ok = res.hasCanvas && res.canvasW > 0 && res.canvasH > 0 && res.nodeViews > 0 && res.nodeChildren > 0;
|
|
console.log(`\n ${ok ? 'PASS' : 'FAIL'} — canvas ${res.canvasW}x${res.canvasH}, ${res.nodeViews} nodes, ${res.edgeChildren} edge children`);
|
|
await browser.close();
|
|
process.exit(ok && errors.length === 0 ? 0 : 1);
|
|
})();
|