AvanzaCast/packages/studio-panel/scripts/e2e_simulate_guests.mjs

165 lines
5.9 KiB
JavaScript

import { chromium } from 'playwright';
import fs from 'fs';
const BACKEND = process.env.BACKEND_URL || 'http://127.0.0.1:4000';
const GUESTS = Number(process.env.GUESTS || '2');
const HEADLESS = process.env.HEADLESS !== 'false';
const VERBOSE_LOG = '/tmp/e2e_sim_verbose.log';
function vlog(...args){
const line = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ');
try { fs.appendFileSync(VERBOSE_LOG, line + '\n'); } catch(e){}
console.log(line);
}
async function createSession(username) {
vlog('createSession ->', username);
try {
const res = await fetch(`${BACKEND}/api/session`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ room: 'studio-demo', username }),
// keepAlive not available in fetch, but backend should be reachable
});
if (!res.ok) {
const txt = await res.text().catch(()=>'<no-body>');
throw new Error(`session create failed ${res.status} ${txt}`);
}
const j = await res.json();
vlog('session created', j.redirectUrl || j);
return j;
} catch (e) {
vlog('createSession error', String(e));
throw e;
}
}
async function checkBackend(){
try {
vlog('Checking backend health at', BACKEND + '/health');
const r = await fetch(`${BACKEND}/health`, { method: 'GET', cache: 'no-store' , redirect: 'follow' });
if (!r.ok) { vlog('backend health not ok', r.status); return false; }
const j = await r.json().catch(()=>null);
vlog('backend health response', j || '<no-json>');
return true;
} catch (e) { vlog('backend health check failed', String(e)); return false; }
}
(async ()=>{
try{
// reset verbose log
try{ fs.writeFileSync(VERBOSE_LOG, ''); } catch(e){}
vlog('E2E simulate guests - starting', { BACKEND, GUESTS, HEADLESS });
const ok = await checkBackend();
if (!ok) {
vlog('Backend not available at', BACKEND, ' -> aborting');
process.exit(2);
}
// create sessions
const sessions = [];
try {
const mod = await createSession('moderator-e2e');
sessions.push({ role: 'moderator', ...mod });
for (let i=0;i<GUESTS;i++){
const u = `guest-e2e-${i+1}`;
const s = await createSession(u);
sessions.push({ role: 'guest', ...s });
}
} catch (e) {
vlog('failed creating sessions', String(e));
process.exit(2);
}
vlog('Launching browser');
const browser = await chromium.launch({
headless: HEADLESS,
args: [
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
'--allow-file-access-from-files',
'--allow-insecure-localhost'
]
});
const contexts = [];
const pages = [];
try {
// open moderator first
const modSession = sessions.find(s=>s.role==='moderator');
if (!modSession) throw new Error('no moderator session');
const modContext = await browser.newContext({ permissions: ['camera','microphone'] });
const modPage = await modContext.newPage();
modPage.on('console', m => vlog('MOD PAGE:', m.text()));
vlog('Opening moderator at', modSession.redirectUrl);
await modPage.goto(modSession.redirectUrl, { waitUntil: 'networkidle', timeout: 60000 });
contexts.push(modContext); pages.push(modPage);
// open guests
for (let s of sessions.filter(x=>x.role==='guest')){
const ctx = await browser.newContext({ permissions: ['camera','microphone'] });
const pg = await ctx.newPage();
pg.on('console', m => vlog('GUEST PAGE:', m.text()));
vlog('Opening guest at', s.redirectUrl);
await pg.goto(s.redirectUrl, { waitUntil: 'networkidle', timeout: 60000 });
contexts.push(ctx); pages.push(pg);
}
// wait short
await modPage.waitForTimeout(2000);
// click Conectar todos on moderator
vlog('Clicking Conectar todos');
await modPage.evaluate(()=>{
const btn = Array.from(document.querySelectorAll('button')).find(b => {
const txt = (b.textContent||'').trim().toLowerCase();
return txt.includes('conectar todos') || txt.includes('connect all');
});
if (btn && typeof btn.click === 'function') btn.click();
});
// wait for invites to be processed; wait until an svg line has green stroke
vlog('Waiting for accepted lines...');
const accepted = await modPage.waitForFunction(()=>{
const svg = document.querySelector('.connections-overlay');
if (!svg) return false;
const lines = Array.from(svg.querySelectorAll('line'));
return lines.some(l => ((l.getAttribute('stroke')||'').toLowerCase() === '#34d399'));
}, { timeout: 20000 }).catch(()=>null);
if (accepted) vlog('Accepted detected'); else vlog('No accepted detected within timeout');
// screenshots
const outDir = 'packages/studio-panel/.playwright-mcp';
try{ fs.mkdirSync(outDir, { recursive: true }); } catch(e){}
const modShot = '/tmp/e2e_sim_moderator.png';
await modPage.screenshot({ path: modShot, fullPage:true });
fs.copyFileSync(modShot, `${outDir}/${modShot.split('/').pop()}`);
vlog('Saved moderator screenshot to', modShot);
for (let i=0;i<pages.length;i++){
const p = pages[i];
const shot = `/tmp/e2e_sim_page_${i}.png`;
await p.screenshot({ path: shot, fullPage:true });
try{ fs.copyFileSync(shot, `${outDir}/${shot.split('/').pop()}`); } catch(e){}
vlog('Saved guest screenshot to', shot);
}
vlog('E2E simulate finished; screenshots saved');
} catch (e) {
vlog('E2E run failed', String(e));
} finally {
try { await browser.close(); } catch(e){}
}
vlog('Done.');
process.exit(0);
}catch(e){
try{ fs.appendFileSync(VERBOSE_LOG, 'fatal:' + String(e) + '\n'); } catch(_){}
console.error('fatal error', e);
process.exit(3);
}
})();