#!/usr/bin/env node // E2E test (CommonJS) with instrumentation: create session on backend-api, connect to browserless, // open Broadcast Panel, automate UI (create transmission if needed), postMessage token and wait for Studio overlay. // Instrumentation: capture page console, page errors, network requests/responses, trace, HTML snapshot, screenshots and logs. const fs = require('fs') const path = require('path') const fetch = require('node-fetch') const puppeteer = require('puppeteer-core') const ARTIFACT_DIR = '/tmp/avanzacast_e2e' if (!fs.existsSync(ARTIFACT_DIR)) fs.mkdirSync(ARTIFACT_DIR, { recursive: true }) function now() { return new Date().toISOString().replace(/[:.]/g, '-') } ;(async () => { const START_TS = Date.now() const logFile = path.join(ARTIFACT_DIR, 'e2e.log') try { fs.writeFileSync(logFile, '') } catch(e){} const log = (msg, ...rest) => { const t = new Date().toISOString() const extra = rest && rest.length ? ' ' + rest.map(r => (typeof r === 'string' ? r : JSON.stringify(r))).join(' ') : '' const line = `[${t}] ${msg}${extra}` console.log(line) try { fs.appendFileSync(logFile, line + '\n') } catch(e){} } try { const BACKEND = process.env.TOKEN_SERVER || 'http://localhost:4000' const BROADCAST_URL = process.env.BROADCAST_URL || 'https://avanzacast-broadcastpanel.bfzqqk.easypanel.host' const BROWSERLESS_TOKEN = process.env.BROWSERLESS_TOKEN || process.env.BROWSERLESS || process.env.TOKEN || '' if (!BROWSERLESS_TOKEN) { log('BROWSERLESS_TOKEN not set in env (use BROWSERLESS_TOKEN=...)') process.exit(1) } // timeouts and retries const OVERLAY_WAIT_MS = 60 * 1000 // 60s const POLL_INTERVAL_MS = 1000 log('Creating session on backend:', BACKEND) const res = await fetch(`${BACKEND.replace(/\/$/, '')}/api/session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room: 'e2e-room', username: 'e2e-runner' }) }) log('Session create status:', res.status) const raw = await res.text().catch(() => '') log('Session raw response:', raw) if (!res.ok) { const t = raw throw new Error('Failed to create session: ' + res.status + ' ' + t) } const j = JSON.parse(raw) log('Parsed session keys:', Object.keys(j)) const livekitToken = j.token const livekitUrl = j.url || j.studioUrl || j.redirectUrl || '' log('Got session token (trunc):', livekitToken ? livekitToken.slice(0, 40) + '...' : '(none)') const wsEndpoint = `wss://browserless.bfzqqk.easypanel.host?token=${BROWSERLESS_TOKEN}` log('Connecting to browserless WS endpoint:', wsEndpoint) const browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint, defaultViewport: { width: 1400, height: 900 } }) const page = await browser.newPage() // Instrumentation containers const networkEvents = [] const consoleEvents = [] const pageErrors = [] // Capture console page.on('console', async msg => { try { const args = [] for (let i = 0; i < msg.args().length; i++) { try { const a = await msg.args()[i].jsonValue(); args.push(a) } catch(e) { args.push(msg.args()[i].toString()) } } const text = msg.text() consoleEvents.push({ ts: Date.now(), type: msg.type(), text, args }) log('[page console]', msg.type(), text, args.length ? JSON.stringify(args) : '') } catch(e) { log('[page console error]', e.message) } }) page.on('pageerror', err => { pageErrors.push({ ts: Date.now(), error: String(err && err.stack ? err.stack : err) }) log('[page error]', err && err.stack ? err.stack : String(err)) }) // Capture network requests/responses page.on('request', req => { try { networkEvents.push({ type: 'request', ts: Date.now(), url: req.url(), method: req.method(), headers: req.headers() }) } catch(e){} }) page.on('response', async res => { try { const url = res.url() const status = res.status() const headers = res.headers() let body = '' try { const ct = headers['content-type'] || '' if (ct.includes('application/json') || ct.includes('text/') || ct.includes('application/javascript')) { body = await res.text() if (body && body.length > 2000) body = body.slice(0, 2000) + '...[truncated]' } } catch(e) { body = '[error reading body]' } networkEvents.push({ type: 'response', ts: Date.now(), url, status, headers, body }) } catch(e){} }) page.on('requestfailed', req => { networkEvents.push({ type: 'requestfailed', ts: Date.now(), url: req.url(), err: req.failure && req.failure.errorText }) }) // Start tracing const traceFile = path.join(ARTIFACT_DIR, `trace-${now()}.json`) try { await page.tracing.start({ path: traceFile, screenshots: true }) log('Started page tracing ->', traceFile) } catch (e) { log('Tracing start failed:', e.message) } // Navigate with token in URL (preferred) const encodedUrl = encodeURIComponent(livekitUrl || '') const urlWithToken = `${BROADCAST_URL.replace(/\/$/, '')}?token=${encodeURIComponent(livekitToken || '')}&url=${encodedUrl}&room=${encodeURIComponent(j.room || 'e2e-room')}` log('Navigating to broadcast URL with token') await page.goto(urlWithToken, { waitUntil: 'networkidle2', timeout: 120000 }) // Give SPA time to hydrate await page.waitForTimeout(1500) // Helper to try clicking buttons by text (robust) async function clickButtonByText(texts, timeout = 3000) { const start = Date.now() while (Date.now() - start < timeout) { for (const t of texts) { const handles = await page.$x(`//button[contains(normalize-space(.), "${t}")]`) if (handles && handles.length > 0) { try { await handles[0].click(); log('Clicked button', t); return true } catch(e) { log('Click error', e.message) } } } await page.waitForTimeout(300) } return false } // Wait for UI try { await page.waitForFunction(() => !!document.querySelector('body'), { timeout: 20000 }); log('Page body present') } catch(e) { log('Page body missing?') } // Try to click Entrar al estudio and follow creation flow const clickedEntrar = await clickButtonByText(['Entrar al estudio'], 8000) if (clickedEntrar) { log('Clicked Entrar al estudio (first attempt)') // click 'Omitir ahora' if appears await clickButtonByText(['Omitir ahora', 'Omitar ahora', 'Skip for now', 'Skip'], 4000) // fill modal input try { const inputHandle = await page.$(".modal input, .Dialog input, dialog input, input[placeholder*='Título'], input[placeholder*='titulo'], input[placeholder*='Transmi'], input[type='text']") if (inputHandle) { try { await inputHandle.click({ clickCount: 3 }) } catch(e){}; await inputHandle.type('Transmision en Vivo', { delay: 40 }); log('Filled transmission title input') } } catch(e){ log('fill title failed', e.message) } await clickButtonByText(['Empezar ahora', 'Empezar', 'Iniciar', 'Start now', 'Start'], 5000) await page.waitForTimeout(800) await clickButtonByText(['Entrar al estudio'], 5000) } else { log('Entrar al estudio not clickable initially; proceeding to postMessage fallback') } // Post token via postMessage const payload = { type: 'LIVEKIT_TOKEN', token: livekitToken, url: livekitUrl, room: j.room || 'e2e-room' } log('Posting payload (trunc):', { type: payload.type, token: payload.token ? payload.token.slice(0,40) + '...' : null, url: payload.url, room: payload.room }) try { await page.evaluate((p) => { window.postMessage(p, window.location.origin); return true }, payload); log('postMessage executed') } catch(e) { log('postMessage failed', e.message) } // Wait for overlay with polling const overlaySelectors = ['.studio-portal', '.studioOverlay', '.studio-portal__center', '.studio-overlay'] const overlayStart = Date.now() let overlayFound = false while (Date.now() - overlayStart < OVERLAY_WAIT_MS) { for (const sel of overlaySelectors) { const el = await page.$(sel) if (el) { overlayFound = true; log('Overlay detected by selector', sel); break } } if (overlayFound) break await page.waitForTimeout(POLL_INTERVAL_MS) } if (!overlayFound) log('Overlay not detected within timeout, capturing extra artifacts for debugging') else log('Overlay successfully detected') // Save artifacts const screenshotPath = path.join(ARTIFACT_DIR, `screenshot-${now()}.png`) await page.screenshot({ path: screenshotPath, fullPage: false }) log('Screenshot saved to', screenshotPath) try { const html = await page.content(); const htmlPath = path.join(ARTIFACT_DIR, `page-${now()}.html`); fs.writeFileSync(htmlPath, html); log('Saved page HTML to', htmlPath) } catch(e){ log('Failed saving HTML', e.message) } try { const netPath = path.join(ARTIFACT_DIR, `network-${now()}.json`); fs.writeFileSync(netPath, JSON.stringify(networkEvents, null, 2)); log('Saved network events to', netPath) } catch(e){ log('Failed saving network events', e.message) } try { const consolePath = path.join(ARTIFACT_DIR, `console-${now()}.json`); fs.writeFileSync(consolePath, JSON.stringify(consoleEvents, null, 2)); log('Saved console events to', consolePath) } catch(e){ log('Failed saving console events', e.message) } try { const pageErrorsPath = path.join(ARTIFACT_DIR, `page-errors-${now()}.json`); fs.writeFileSync(pageErrorsPath, JSON.stringify(pageErrors, null, 2)); log('Saved page errors to', pageErrorsPath) } catch(e){ log('Failed saving page errors', e.message) } try { await page.tracing.stop(); log('Stopped tracing; trace saved to', traceFile) } catch(e){ log('Tracing stop failed', e.message) } await browser.close() log('Browser closed') const duration = (Date.now() - START_TS) / 1000 log('E2E run finished in', duration + 's') const files = fs.readdirSync(ARTIFACT_DIR).map(f => path.join(ARTIFACT_DIR, f)) log('Artifacts produced:', files) process.exit(overlayFound ? 0 : 3) } catch (err) { try { fs.appendFileSync(path.join(ARTIFACT_DIR, 'e2e.log'), String(err && err.stack ? err.stack : err) + '\n') } catch(e){} console.error('E2E failed:', err && err.stack ? err.stack : err) process.exit(2) } })()