AvanzaCast/packages/broadcast-panel/e2e/generate_visual_baseline.js

223 lines
10 KiB
JavaScript

#!/usr/bin/env node
/*
generate_visual_baseline.js
- Creates a session on the token server
- Connects to a remote Chrome with debugging enabled (default http://localhost:9222),
or to browserless via WS/CDP using puppeteer-core if remote-debug is not available
- Navigates to studioUrl and posts token if present
- Saves a screenshot to e2e/baseline/studio.png and e2e/out/<timestamp>/studio.png
*/
// Diagnostic helpers early to surface environment issues quickly
try {
console.log('=== generate_visual_baseline start ===');
console.log('cwd:', process.cwd());
console.log('node version:', process.version);
console.log('SCRIPT: generate_visual_baseline.js');
console.log('ENV: REMOTE_DEBUG, REMOTE_DEBUG_WS, BROWSERLESS_WS, BROWSERLESS_TOKEN, TOKEN_SERVER, BROADCAST_URL');
console.log('REMOTE_DEBUG=', process.env.REMOTE_DEBUG, 'REMOTE_DEBUG_WS=', process.env.REMOTE_DEBUG_WS || process.env.REMOTE_WS, 'REMOTE_DEBUG_PORT=', process.env.REMOTE_DEBUG_PORT);
console.log('BROWSERLESS_WS=', process.env.BROWSERLESS_WS, 'BROWSERLESS_TOKEN=', !!process.env.BROWSERLESS_TOKEN);
console.log('TOKEN_SERVER=', process.env.TOKEN_SERVER || process.env.BACKEND);
console.log('BROADCAST_URL=', process.env.BROADCAST_URL);
} catch (e) { /* ignore */ }
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION:', err && err.stack ? err.stack : err);
process.exit(2);
});
process.on('unhandledRejection', (reason) => {
console.error('UNHANDLED REJECTION:', reason && reason.stack ? reason.stack : reason);
process.exit(2);
});
const fetch = global.fetch ? global.fetch : (...args) => import('node-fetch').then(m => m.default(...args));
const puppeteer = require('puppeteer-core');
const fs = require('fs');
const path = require('path');
// Enhanced remote debug resolution: support REMOTE_DEBUG_ADDRESS + REMOTE_DEBUG_PORT
const REMOTE_DEBUG_ADDRESS = process.env.REMOTE_DEBUG_ADDRESS || process.env.REMOTE_DEBUG_HOST || 'localhost';
const REMOTE_DEBUG_PORT = process.env.REMOTE_DEBUG_PORT || process.env.REMOTE_PORT || '';
let REMOTE_DEBUG_WS = process.env.REMOTE_WS || process.env.REMOTE_DEBUG || '';
if (!REMOTE_DEBUG_WS) {
if (REMOTE_DEBUG_PORT) {
REMOTE_DEBUG_WS = `http://${REMOTE_DEBUG_ADDRESS}:${REMOTE_DEBUG_PORT}`;
} else {
REMOTE_DEBUG_WS = 'http://localhost:9222';
}
}
const BROWSERLESS_WS = process.env.BROWSERLESS_WS || process.env.REMOTE_WSE || 'wss://browserless.bfzqqk.easypanel.host';
const BROWSERLESS_TOKEN = process.env.BROWSERLESS_TOKEN || process.env.BROWSERLESS_KEY || process.env.BROWSERLESS || '';
const TOKEN_SERVER = process.env.TOKEN_SERVER || process.env.BACKEND || 'https://avanzacast-servertokens.bfzqqk.easypanel.host';
// Cambiar BROADCAST default para seguir la estructura rooms/<room>
const BROADCAST_BASE = process.env.BROADCAST_BASE || process.env.STUDIO_BASE || 'http://localhost:5175';
const ROOM = process.env.ROOM || 'iuqiw-aksjka';
const BROADCAST = process.env.BROADCAST_URL || `${BROADCAST_BASE.replace(/\/$/, '')}/rooms/${encodeURIComponent(ROOM)}`;
const CDP_RESOLVE_RETRIES = Number(process.env.CDP_RESOLVE_RETRIES || 6);
const CDP_RESOLVE_INTERVAL = Number(process.env.CDP_RESOLVE_INTERVAL_MS || 2000);
function log(...args){
try { console.log(...args); } catch (e) {}
try {
const outdir = path.join(process.cwd(), 'e2e', 'out');
try { fs.mkdirSync(outdir, { recursive: true }); } catch(e) {}
const logfile = path.join(outdir, 'generate_visual_baseline.log');
try { fs.appendFileSync(logfile, `[${new Date().toISOString()}] ${args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}\n`); } catch(e) {}
} catch (e) {}
}
async function createSession(){
log('Creating session on token server', TOKEN_SERVER, ROOM);
const res = await fetch(`${TOKEN_SERVER.replace(/\/$/, '')}/api/session`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ room: ROOM, username: 'visual-runner', ttl: 300 }),
});
const text = await res.text(); try { return JSON.parse(text); } catch(e){ return null; }
}
async function resolveRemoteWSEndpoint(raw){
if (!raw) return null;
raw = String(raw).trim();
// if already a ws:// or wss:// endpoint, return as-is
if (raw.startsWith('ws://') || raw.startsWith('wss://')) return raw;
// if given as a numeric port, assume localhost:port
if (/^\d+$/.test(raw)) raw = `http://localhost:${raw}`;
// if it's missing scheme, assume http
if (!raw.startsWith('http://') && !raw.startsWith('https://')) raw = `http://${raw}`;
try{
const ver = await fetch(raw.replace(/\/$/, '') + '/json/version');
if (ver && ver.ok){ const j = await ver.json(); if (j.webSocketDebuggerUrl) return j.webSocketDebuggerUrl; }
}catch(e){ log('resolveRemoteWSEndpoint /json/version error', String(e)); }
try{
const list = await fetch(raw.replace(/\/$/, '') + '/json/list');
if (list && list.ok){ const arr = await list.json(); if (Array.isArray(arr) && arr.length && arr[0].webSocketDebuggerUrl) return arr[0].webSocketDebuggerUrl; }
}catch(e){ log('resolveRemoteWSEndpoint /json/list error', String(e)); }
try{
const j = await fetch(raw.replace(/\/$/, '') + '/json');
if (j && j.ok){ const arr = await j.json(); if (Array.isArray(arr) && arr.length && arr[0].webSocketDebuggerUrl) return arr[0].webSocketDebuggerUrl; }
}catch(e){ log('resolveRemoteWSEndpoint /json error', String(e)); }
return null;
}
async function waitForCDP(raw, retries = CDP_RESOLVE_RETRIES, interval = CDP_RESOLVE_INTERVAL){
let attempt = 0;
while(attempt < retries){
attempt++;
try{
log(`CDP resolve attempt ${attempt}/${retries} for ${raw}`);
const resolved = await resolveRemoteWSEndpoint(raw);
if (resolved) return resolved;
}catch(e){ log('waitForCDP attempt error', String(e)); }
await new Promise(r => setTimeout(r, interval));
}
return null;
}
(async ()=>{
try{
const session = await createSession();
if (!session) throw new Error('Failed to create session');
log('Session created:', session.id, 'studioUrl=', session.studioUrl || session.url);
const studioUrl = session.studioUrl || session.redirectUrl || session.url || BROADCAST;
let token = session.token || null;
if (!token && session.id) {
// try GET
try{
const getResp = await fetch(`${TOKEN_SERVER.replace(/\/$/, '')}/api/session/${encodeURIComponent(session.id)}`);
const txt = await getResp.text(); const json = JSON.parse(txt);
if (json && json.token) token = json.token;
}catch(e){ log('GET session token failed', String(e)); }
}
// Try to resolve CDP endpoint from remote debug first
let connectEndpoint = null;
if (REMOTE_DEBUG_WS) {
try{
const resolved = await waitForCDP(REMOTE_DEBUG_WS);
if (resolved) {
connectEndpoint = resolved;
log('Resolved remote-debug CDP endpoint:', connectEndpoint);
} else {
log('No CDP endpoint discovered at remote-debug URL after retries:', REMOTE_DEBUG_WS);
}
} catch(e){ log('Error resolving remote-debug endpoint', String(e)); }
}
// If remote debug not available, try browserless
if (!connectEndpoint && BROWSERLESS_WS) {
try{
if ((BROWSERLESS_WS.startsWith('ws://') || BROWSERLESS_WS.startsWith('wss://')) && BROWSERLESS_TOKEN) {
connectEndpoint = `${BROWSERLESS_WS}${BROWSERLESS_WS.includes('?') ? '&' : '?'}token=${encodeURIComponent(BROWSERLESS_TOKEN)}`;
} else {
connectEndpoint = await waitForCDP(BROWSERLESS_WS + (BROWSERLESS_TOKEN ? (BROWSERLESS_WS.includes('?') ? '&' : '?') + `token=${encodeURIComponent(BROWSERLESS_TOKEN)}` : ''));
if (!connectEndpoint) connectEndpoint = await waitForCDP(BROWSERLESS_WS);
}
log('Browserless resolution result:', connectEndpoint || '(none)');
}catch(e){ log('Browserless resolve error', String(e)); }
}
log('connectEndpoint resolved to', connectEndpoint || '(none)');
let browser = null;
if (connectEndpoint) {
try{
browser = await puppeteer.connect({ browserWSEndpoint: connectEndpoint, timeout: 30000, ignoreHTTPSErrors: true });
log('Connected to remote browser via puppeteer (CDP)');
}catch(e){
log('puppeteer.connect failed:', String(e));
browser = null;
}
}
if (!browser) {
log('Launching local puppeteer fallback');
browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
}
const page = await browser.newPage();
page.on('console', msg => { try{ log('[BROWSER]', msg.type(), msg.text()); } catch(e){} });
page.on('pageerror', err => log('[PAGEERROR]', err && err.stack ? err.stack : String(err)));
log('Navigating to', studioUrl);
await page.goto(studioUrl, { waitUntil: 'networkidle2' }).catch(e=>{ log('page.goto failed', String(e)); });
await page.waitForTimeout(1200);
if (token) {
try{
log('Posting token to page via postMessage (length', token.length, ')');
await page.evaluate((tk)=>{ try{ window.postMessage({ type:'LIVEKIT_TOKEN', token: tk, room: '', url: window.location.href }, window.location.origin); } catch(e){} }, token);
}catch(e){ log('postMessage evaluate failed', String(e)); }
}
await page.waitForTimeout(1200);
const baselineDir = path.join(process.cwd(), 'e2e', 'baseline');
try{ fs.mkdirSync(baselineDir, { recursive: true }); } catch(e){}
const outDir = path.join(process.cwd(), 'e2e', 'out', `visual_${Date.now()}`);
try{ fs.mkdirSync(outDir, { recursive: true }); } catch(e){}
const baselinePath = path.join(baselineDir, 'studio.png');
const outPath = path.join(outDir, 'studio.png');
await page.screenshot({ path: outPath, fullPage: false });
log('Saved capture to', outPath);
// Copy to baseline if not exists or if FORCE_BASELINE=1
const force = process.env.FORCE_BASELINE === '1';
if (!fs.existsSync(baselinePath) || force) {
try{ fs.copyFileSync(outPath, baselinePath); log('Wrote baseline to', baselinePath); } catch(e){ log('Failed writing baseline', String(e)); }
} else {
log('Baseline exists at', baselinePath, '(not overwritten)');
}
try{ await page.close(); } catch(e){}
try{ await browser.close(); } catch(e){}
log('Done');
process.exit(0);
}catch(err){
console.error('Fatal', err && err.stack ? err.stack : err);
process.exit(2);
}
})();