Files
root 30f8052e79 Add viewport regression test for the topology layout fix
The off-screen-layout bug (nodes centred on the scrollable width instead of the
viewport) only reproduced at the default window size, so a single-width test
would have shipped it. Verifies the graph stays framed at 1920/1400/1100/820,
and records which assumption in the pixel census is safe: the measurement uses
the canvas buffer size, not the screen size, because the CSS min-width makes
narrow viewports scroll.
2026-09-27 00:39:07 +08:00

76 lines
3.2 KiB
JavaScript

// The off-screen-layout bug only showed at the default width. Prove the fix
// holds across viewport sizes: the graph must stay framed at every size, and
// zooming/panning must keep the whole graph reachable.
const { chromium } = require('/tmp/gltest/node_modules/playwright-core');
const BASE = process.env.PANEL_URL || 'https://panel.attackdefense.imrnes.team';
const SIZES = [
{ w: 1920, h: 1080, label: 'desktop-wide' },
{ w: 1400, h: 900, label: 'laptop' },
{ w: 1100, h: 800, label: 'narrow' },
{ w: 820, h: 900, label: 'below-canvas-min' },
];
(async () => {
const browser = await chromium.launch({
args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-unsafe-swiftshader'],
});
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;
}, {});
let failures = 0;
for (const s of SIZES) {
const page = await browser.newPage({ viewport: { width: s.w, height: s.h } });
const errs = [];
page.on('pageerror', e => errs.push(e.message));
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(2200);
const r = await page.evaluate(() => {
const g = window.__topoProbe();
const rd = g.app.renderer;
rd.render(g.app.stage);
const W = rd.width, H = rd.height;
const buf = new Uint8Array(W * H * 4);
rd.gl.readPixels(0, 0, W, H, rd.gl.RGBA, rd.gl.UNSIGNED_BYTE, buf);
let nonBg = 0, 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;
if (Math.max(Math.abs(buf[i]-10), Math.abs(buf[i+1]-15), Math.abs(buf[i+2]-28)) > 3) {
nonBg++;
if (x < minX) minX = x; if (x > maxX) maxX = x;
if (y < minY) minY = y; if (y > maxY) maxY = y;
}
}
// A viewport narrower than the CSS min-width scrolls; the visible part of
// the canvas is what matters, so measure the canvas buffer, not the screen.
return {
W, H, pct: +(100 * nonBg / (W * H)).toFixed(2),
bbox: maxX < 0 ? null : [minX, minY, maxX, maxY],
inFrame: maxX < 0 ? false : (minX >= 0 && minY >= 0 && maxX <= W && maxY <= H),
scale: +g.scale.toFixed(3),
nodes: g.nodeViews.size,
glErr: rd.gl.getError(),
};
});
const ok = r.pct > 1 && r.inFrame && r.glErr === 0 && r.nodes > 0 && errs.length === 0;
if (!ok) failures++;
console.log(` ${ok ? 'PASS' : 'FAIL'} ${s.label.padEnd(20)} canvas ${r.W}x${r.H} drawn ${String(r.pct).padStart(5)}% scale ${r.scale} bbox ${JSON.stringify(r.bbox)}${errs.length ? ' ERR ' + errs[0] : ''}`);
await page.close();
}
await browser.close();
console.log(`\n ${failures === 0 ? 'PASS' : 'FAIL'} — ${SIZES.length - failures}/${SIZES.length} viewports framed correctly`);
process.exit(failures === 0 ? 0 : 1);
})();