- 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
235 lines
12 KiB
JavaScript
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) })
|