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