diff --git a/panel/test_topo_viewports.js b/panel/test_topo_viewports.js new file mode 100644 index 0000000..7bff485 --- /dev/null +++ b/panel/test_topo_viewports.js @@ -0,0 +1,75 @@ +// 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); +})();