AvanzaCast/e2e/puppeteer-runner/send-token-to-studio.js
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

235 lines
12 KiB
JavaScript

(async ()=>{
const mod = await import('puppeteer')
const puppeteer = (mod && mod.default) ? mod.default : mod
const path = await import('path')
const fs = await import('fs')
const chromePath = process.env.CHROME_PATH || '/usr/bin/chromium'
const url = process.env.VITE_BROADCASTPANEL_URL || 'https://avanzacast-broadcastpanel.bfzqqk.easypanel.host'
const token = process.env.TOKEN || 'e2e098863b912f6a178b68e71ec3c58d'
const livekitUrl = process.env.VITE_LIVEKIT_WS_URL || 'wss://livekit-server.bfzqqk.easypanel.host'
const outDir = process.cwd()
console.log('Launching Chromium at', chromePath)
const browser = await puppeteer.launch({ executablePath: chromePath, args: ['--no-sandbox','--disable-setuid-sandbox','--disable-dev-shm-usage'], defaultViewport: { width: 1400, height: 900 }, headless: 'new' })
const page = await browser.newPage()
const consoles = []
page.on('console', msg => { try { consoles.push({ type: msg.type(), text: msg.text() }) } catch(e) {} })
const pageErrors = []
page.on('pageerror', err => { pageErrors.push(String(err && err.stack ? err.stack : err)) })
try {
console.log('Navigating to', url)
await page.goto(url, { waitUntil: 'networkidle2', timeout: 90000 })
console.log('Page loaded')
} catch(e){ console.warn('goto error', e && e.message?e.message:e) }
await page.waitForTimeout(2000)
// Try to click 'Transmisión en vivo' card to open transmissions panel
const navClicked = await page.evaluate(()=>{
const norm = (s)=> (s||'').toLowerCase().trim()
// 1) Try within create grid
try{
const grid = document.querySelector('[class*="createGrid"], .createGrid')
if(grid){
const buttons = Array.from(grid.querySelectorAll('button, a'))
for(const b of buttons){
const t = norm(b.textContent || b.innerText || b.getAttribute('aria-label') || '')
if(t.includes('transmisión en vivo') || t.includes('transmision en vivo') || t.includes('transmis')){
try{ b.click(); return {ok:true, from:'createGrid', text:t.slice(0,120)} }catch(e){}
}
}
}
}catch(e){}
// 2) exact text anywhere among buttons/links
const nodes = Array.from(document.querySelectorAll('button, a, [role="button"]'))
const hits = []
for(const n of nodes){
const t = norm(n.textContent || n.innerText || n.getAttribute('aria-label') || '')
if(!t) continue
if(t === 'transmisión en vivo' || t === 'transmision en vivo' || t.includes('transmis')){
try{ n.click(); return {ok:true, from:'global', text:t.slice(0,120)} }catch(e){}
}
if(t.includes('transmis')) hits.push({text:t,tag:n.tagName,class:n.className.slice(0,120),aria:n.getAttribute('aria-label')||''})
}
// expose hits to page console for debugging
try{ console.debug('transmis-candidates', JSON.stringify(hits.slice(0,20))) }catch(e){}
return {ok:false, hits: hits.slice(0,10)}
})
console.log('navClicked:', navClicked)
await page.waitForTimeout(2000)
// Wait for 'Entrar al estudio' button to appear (retry for up to 10s)
let foundEnter = false
for(let attempt=0; attempt<8; attempt++){
foundEnter = await page.evaluate(()=>{
const nodes = Array.from(document.querySelectorAll('button, a, [role="button"]'))
const norm = (s)=> (s||'').toLowerCase().trim()
for(const n of nodes){ const t = norm(n.textContent||n.getAttribute('aria-label')||n.innerText||''); if(t.includes('entrar al estudio') || t === 'entrar al estudio' || t.includes('entrando...')) return true }
return false
})
console.log('check enter button attempt', attempt, 'found=', foundEnter)
if(foundEnter) break
await page.waitForTimeout(1000 + attempt*500)
}
// If enter button does not exist, attempt to create a new transmission via UI flow
if(!foundEnter){
console.log('No Entrar al estudio found — attempting to create a transmission via UI')
try{
// Attempt to click a 'Transmisión en vivo' or 'Nueva transmisión' card/button again
await page.evaluate(()=>{
const nodes = Array.from(document.querySelectorAll('button, a, div'))
const norm = (s)=> (s||'').toLowerCase().trim()
for(const n of nodes){
const t = norm(n.textContent || n.innerText || n.getAttribute('aria-label') || '')
if(!t) continue
if(t.includes('transmisión en vivo') || t.includes('transmisión') || t.includes('nueva escena') || t.includes('nueva transmisión') || t.includes('nueva')){
try{ n.click(); break }catch(e){}
}
}
})
await page.waitForTimeout(1200)
// If a modal appears with 'Omitir ahora' (skip scheduling), click it
const clickedOmit = await page.evaluate(()=>{
const nodes = Array.from(document.querySelectorAll('button, a'))
const norm = (s)=> (s||'').toLowerCase().trim()
for(const n of nodes){
const t = norm(n.textContent || n.innerText || n.getAttribute('aria-label') || '')
if(!t) continue
if(t.includes('omitir') || t.includes('omitir ahora') || t.includes('skip') ){
try{ n.click(); return {ok:true, text:t.slice(0,120)} }catch(e){ return {ok:false, err:String(e)} }
}
}
return {ok:false}
})
console.log('clickedOmit result', clickedOmit)
await page.waitForTimeout(1000)
// Fill title input if present and click 'Empezar ahora' or similar
const created = await page.evaluate(()=>{
// Try to find an input/textarea for title
const inputs = Array.from(document.querySelectorAll('input, textarea'))
const norm = (s)=> (s||'').toLowerCase().trim()
let filled = false
for(const inp of inputs){
try{
const p = inp.getAttribute('placeholder') || inp.getAttribute('aria-label') || inp.name || ''
const t = norm(p)
if(t.includes('tít') || t.includes('titu') || t.includes('title') || t.includes('nombre') || t.includes('transmisi')){
try{ inp.focus(); inp.value = 'Transmision'; inp.dispatchEvent(new Event('input', { bubbles: true })); }catch(e){}
filled = true; break
}
}catch(e){}
}
// If not filled, try first visible input
if(!filled && inputs.length>0){ try{ inputs[0].focus(); inputs[0].value = 'Transmision'; inputs[0].dispatchEvent(new Event('input', { bubbles: true })); filled = true }catch(e){} }
// Find and click 'Empezar ahora' / 'Empezar' / 'Start' / 'Iniciar ahora'
const buttons = Array.from(document.querySelectorAll('button, a'))
for(const b of buttons){
const txt = norm(b.textContent||b.getAttribute('aria-label')||b.innerText||'')
if(txt.includes('empezar ahora') || txt.includes('empezar') || txt.includes('iniciar ahora') || txt.includes('start now') || txt.includes('comenzar')){
try{ b.click(); return {ok:true, clicked: txt} }catch(e){ return {ok:false, err:String(e)} }
}
}
return {ok:false, filled}
})
console.log('created transmission result', created)
await page.waitForTimeout(2000)
// After creation, re-check for Enter button
for(let attempt=0; attempt<8; attempt++){
foundEnter = await page.evaluate(()=>{
const nodes = Array.from(document.querySelectorAll('button, a, [role="button"]'))
const norm = (s)=> (s||'').toLowerCase().trim()
for(const n of nodes){ const t = norm(n.textContent||n.getAttribute('aria-label')||n.innerText||''); if(t.includes('entrar al estudio') || t === 'entrar al estudio' || t.includes('entrando...')) return true }
return false
})
console.log('re-check enter button attempt', attempt, 'found=', foundEnter)
if(foundEnter) break
await page.waitForTimeout(1000 + attempt*500)
}
}catch(e){ console.warn('create transmission flow failed', e && e.message?e.message:e) }
}
// If enter button exists, try to click the first one
let clickedEnter = { ok: false }
if(foundEnter){
clickedEnter = await page.evaluate(()=>{
const nodes = Array.from(document.querySelectorAll('button, a, [role="button"]'))
const norm = (s)=> (s||'').toLowerCase().trim()
for(const n of nodes){ const t = norm(n.textContent||n.getAttribute('aria-label')||n.innerText||''); if(t.includes('entrar al estudio')){ try{ n.click(); return {ok:true, text:t.slice(0,120)} }catch(e){ return {ok:false, err: String(e)} } } }
return {ok:false}
})
console.log('clicked enter button result', clickedEnter)
} else {
console.log('Entrar al estudio button still not found after create attempt — will still postMessage to window')
}
await page.waitForTimeout(1000)
const beforePath = path.join(outDir, 'send-token-before.png')
try { await page.screenshot({ path: beforePath, fullPage: true }); console.log('Saved screenshot', beforePath) } catch(e){ console.warn('screenshot before failed', e && e.message)}
const POST_ORIGIN = '*'
const payload = { type: 'LIVEKIT_TOKEN', token, url: livekitUrl, room: 'e2e-room' }
const result = await page.evaluate(async (payload, POST_ORIGIN)=>{
const tryPost = (w, origin) => { try{ w.postMessage(payload, origin); return true }catch(e){return false} }
const tried = []
try{
const globals = ['__studioPopup','popupForE2E','window.__studioPopup','__AVZ_LAST_MSG_SOURCE','__AVZ_LAST_MSG_SOURCE?.source']
for(const g of globals){ try{ const w = window[g]; if(w && typeof w.postMessage === 'function') { tried.push({target:g,ok:tryPost(w, window.location.origin || '*')}) } }catch(e){} }
if(window.opener && !window.opener.closed){ tried.push({target:'opener', ok:tryPost(window.opener, POST_ORIGIN)}) }
try{ window.postMessage(payload, window.location.origin); tried.push({target:'self', ok:true}) } catch(e) { tried.push({target:'self', ok:false}) }
}catch(e){ tried.push({error: String(e)}) }
return tried
}, payload, POST_ORIGIN)
console.log('postMessage attempts:', result)
// Fallback: if not connected yet, navigate to URL with token query so app auto-opens studio
const fallbackUrl = `${url.replace(/\/$/, '')}/?token=${encodeURIComponent(token)}&room=e2e-room`;
console.log('fallbackUrl:', fallbackUrl)
try{
await page.goto(fallbackUrl, { waitUntil: 'networkidle2', timeout: 15000 })
console.log('Navigated to fallback token URL')
} catch(e){ console.warn('fallback navigation failed', e && e.message)}
// wait and check for Studio status element added by App (id='status' or presence of StudioPortal root)
let connected = false
try {
const maxWait = 20000
const start = Date.now()
while (Date.now() - start < maxWait) {
try {
const statusText = await page.evaluate(()=>{
const el = document.getElementById('status')
if(el) return el.textContent
// try detecting studio overlay by looking for known texts
const nodes = Array.from(document.querySelectorAll('div,span'))
for(const n of nodes){ const t=(n.textContent||'').toLowerCase(); if(t.includes('validando token') || t.includes('validando') || t.includes('entrando') || t.includes('validando token') || t.includes('conectado')) return t }
return null
})
if (statusText && statusText.toLowerCase().includes('conectado')) { connected = true; break }
} catch(e) {}
await page.waitForTimeout(1000)
}
} catch(e) { console.warn('status check failed', e && e.message)}
await page.waitForTimeout(1000)
const afterPath = path.join(outDir, 'send-token-after.png')
try { await page.screenshot({ path: afterPath, fullPage: true }); console.log('Saved screenshot', afterPath) } catch(e){ console.warn('screenshot after failed', e && e.message)}
try {
const out = { consoles, pageErrors, navClicked: navClicked, clickedEnter: clickedEnter, postAttempts: result, connected }
const logPath = path.join(outDir, 'send-token-browser-log.json')
await fs.promises.writeFile(logPath, JSON.stringify(out, null, 2), 'utf8')
console.log('Wrote browser log to', logPath)
} catch(e){ console.warn('failed to write browser log', e && e.message)}
await browser.close()
console.log('done, connected=', connected)
})().catch(e=>{ console.error(e && e.stack?e.stack:e); process.exit(2) })