// The topology "disappears" for the user but passed my tests. Two suspects in // index.html: // 1. The Topology tab button calls `showView('topo'); loadTopo()`, and // showView ITSELF calls loadTopo() when v==='topo' -> two concurrent calls. // 2. ensureTopoGraph() assigns `topoGraph = new TopoGraph(host)` BEFORE awaiting // init(), but the guard is `if (topoGraph && topoGraph.app)` — and app is // null until init() finishes. A second concurrent call therefore builds a // SECOND renderer. Both run host.innerHTML = '' and append their own canvas, // so whichever init() finishes LAST wins the DOM while `topoGraph` still // points at the other one -> a canvas that is running but detached from the // page, i.e. nothing visible. // Hammer the tab exactly like a user clicking it, and check how many canvases // exist and which one the bridge holds. 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 errs = []; page.on('pageerror', e => errs.push('PAGEERROR: ' + e.message)); page.on('console', m => { if (m.type() === 'error') errs.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'); // Count how many times the module actually initialises a renderer. await page.evaluate(() => { window.__inits = 0; const host = document.getElementById('topoHost'); const mo = new MutationObserver(() => { /* canvas churn visible below */ }); mo.observe(host, { childList: true }); window.__mo = mo; }); console.log('=== single tab click (what my tests did) ==='); await page.click('.tab[data-view=topo]'); await page.waitForTimeout(2500); let s = await page.evaluate(() => { const host = document.getElementById('topoHost'); const g = window.__topoProbe ? window.__topoProbe() : null; return { canvases: host.querySelectorAll('canvas').length, hostChildren: host.children.length, bridgeHasApp: !!(g && g.app), bridgeCanvasAttached: !!(g && g.app && g.app.canvas && g.app.canvas.isConnected), nodes: g ? g.nodeViews.size : -1, }; }); console.log(' ', JSON.stringify(s)); console.log('\n=== rapid re-entry (leave tab, come back, x3) ==='); for (let i = 0; i < 3; i++) { await page.click('.tab[data-view=challs]'); await page.waitForTimeout(120); await page.click('.tab[data-view=topo]'); await page.waitForTimeout(900); const t = await page.evaluate(() => { const host = document.getElementById('topoHost'); const g = window.__topoProbe ? window.__topoProbe() : null; return { canvases: host.querySelectorAll('canvas').length, hostChildren: host.children.length, bridgeCanvasAttached: !!(g && g.app && g.app.canvas && g.app.canvas.isConnected), nodes: g ? g.nodeViews.size : -1, }; }); console.log(` round ${i + 1}:`, JSON.stringify(t)); } console.log('\n=== final pixel check on the VISIBLE canvas ==='); const r = await page.evaluate(() => { const host = document.getElementById('topoHost'); const cv = host.querySelector('canvas'); if (!cv) return { error: 'no canvas in host at all' }; 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; for (let i = 0; i < buf.length; i += 4) if (Math.max(Math.abs(buf[i]-10), Math.abs(buf[i+1]-15), Math.abs(buf[i+2]-28)) > 3) nonBg++; return { bridgeCanvasIsTheVisibleOne: cv === g.app.canvas, pct: +(100 * nonBg / (W * H)).toFixed(2), visibleCanvasW: cv.width, bridgeCanvasW: rd.width, }; }); console.log(' ', JSON.stringify(r)); if (errs.length) { console.log('\n errors:'); errs.slice(0, 6).forEach(e => console.log(' ' + e)); } await browser.close(); })();