Files
attack-defense-platform/panel/test_topo_race.js
T

107 lines
4.6 KiB
JavaScript
Raw Normal View History

// 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();
})();