AvanzaCast/packages/broadcast-panel/e2e/puppeteer_browserless_e2e.cjs
Cesar Mendivil 8b458a3ddf feat: add initial LiveKit Meet integration with utility scripts, configs, and core components
- Add Next.js app structure with base configs, linting, and formatting
- Implement LiveKit Meet page, types, and utility functions
- Add Docker, Compose, and deployment scripts for backend and token server
- Provide E2E and smoke test scaffolding with Puppeteer and Playwright helpers
- Include CSS modules and global styles for UI
- Add postMessage and studio integration utilities
- Update package.json with dependencies and scripts for development and testing
2025-11-20 12:50:38 -07:00

209 lines
10 KiB
JavaScript

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