- 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
173 lines
6.8 KiB
JavaScript
173 lines
6.8 KiB
JavaScript
// Debug helper: post a JWT token to BroadcastPanel and wait for StudioPortal to react
|
|
// Usage: E2E_TOKEN=... node debug-post-token.js
|
|
|
|
const puppeteer = require('puppeteer')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
require('dotenv').config()
|
|
|
|
const BROWSERLESS_WS = process.env.BROWSERLESS_WS || process.env.BROWSERLESS || ''
|
|
const CHROME_PATH = process.env.CHROME_PATH || '/usr/bin/chromium'
|
|
const BROADCAST_URL = process.env.VITE_BROADCASTPANEL_URL || 'https://avanzacast-broadcastpanel.bfzqqk.easypanel.host'
|
|
let TOKEN = process.env.E2E_TOKEN || process.env.TOKEN || ''
|
|
const OUT = process.cwd()
|
|
const TOKEN_SERVER = (process.env.VITE_TOKEN_SERVER_URL || process.env.TOKEN_SERVER_URL || '').replace(/\/$/, '')
|
|
|
|
async function fetchTokenFromServer(room = 'e2e-room', username = 'cli-run') {
|
|
if (!TOKEN_SERVER) return null
|
|
try {
|
|
console.info('Requesting token from token server:', TOKEN_SERVER)
|
|
const res = await fetch(`${TOKEN_SERVER}/api/session`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room, username })
|
|
})
|
|
const text = await res.text()
|
|
let json = null
|
|
try { json = JSON.parse(text) } catch (e) { console.warn('Token server returned non-json:', text) }
|
|
if (!res.ok) {
|
|
console.warn('Token server returned', res.status, text)
|
|
return null
|
|
}
|
|
if (json) {
|
|
if (json.token) return json.token
|
|
if (json.redirectUrl) {
|
|
try {
|
|
const u = new URL(json.redirectUrl)
|
|
const t = u.searchParams.get('token')
|
|
if (t) return t
|
|
} catch (e) {}
|
|
}
|
|
if (json.id) {
|
|
// try GET /api/session/:id
|
|
try {
|
|
const r2 = await fetch(`${TOKEN_SERVER}/api/session/${encodeURIComponent(json.id)}`)
|
|
if (r2.ok) {
|
|
const j2 = await r2.json()
|
|
if (j2.token) return j2.token
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
return null
|
|
} catch (e) {
|
|
console.warn('Failed to fetch token from server', e && e.message ? e.message : e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function getBrowser() {
|
|
if (BROWSERLESS_WS) {
|
|
try {
|
|
console.info('Connecting to browserless:', BROWSERLESS_WS)
|
|
return await puppeteer.connect({ browserWSEndpoint: BROWSERLESS_WS, defaultViewport: { width: 1280, height: 800 } })
|
|
} catch (e) {
|
|
console.warn('browserless connect failed', e && e.message)
|
|
}
|
|
}
|
|
console.info('Launching local Chromium at', CHROME_PATH)
|
|
return await puppeteer.launch({ executablePath: CHROME_PATH, headless: 'new', args: ['--no-sandbox','--disable-setuid-sandbox','--disable-dev-shm-usage'], defaultViewport: { width: 1280, height: 800 } })
|
|
}
|
|
|
|
async function run() {
|
|
// if no token, try to fetch one from token server automatically
|
|
if (!TOKEN) {
|
|
console.log('No E2E token provided, attempting to fetch from token server...')
|
|
const fetched = await fetchTokenFromServer()
|
|
if (fetched) {
|
|
TOKEN = fetched
|
|
console.log('Fetched token length=', String(TOKEN).length)
|
|
} else {
|
|
console.error('No token provided and token server fetch failed. Set E2E_TOKEN env var or configure VITE_TOKEN_SERVER_URL')
|
|
process.exit(2)
|
|
}
|
|
}
|
|
|
|
if (!TOKEN) {
|
|
console.error('No token provided. Set E2E_TOKEN env var.')
|
|
process.exit(2)
|
|
}
|
|
const browser = await getBrowser()
|
|
const page = await browser.newPage()
|
|
try {
|
|
page.on('console', msg => console.log('[page]', msg.type(), msg.text()))
|
|
page.on('pageerror', err => console.error('[pageerr]', err && err.stack ? err.stack : err))
|
|
|
|
console.log('Navigating to', BROADCAST_URL)
|
|
await page.goto(BROADCAST_URL, { waitUntil: 'networkidle2', timeout: 60000 })
|
|
await page.waitForTimeout(1000)
|
|
|
|
// clear local state that may contain old tokens
|
|
await page.evaluate(() => {
|
|
try { localStorage.removeItem('lk-user-choices') } catch(e) {}
|
|
try { sessionStorage.clear() } catch(e) {}
|
|
})
|
|
|
|
const shotBefore = path.join(OUT, 'debug-before.png')
|
|
await page.screenshot({ path: shotBefore, fullPage: true })
|
|
console.log('Saved screenshot', shotBefore)
|
|
|
|
const payload = { type: 'LIVEKIT_TOKEN', token: TOKEN, url: process.env.VITE_LIVEKIT_WS_URL || '', room: 'e2e-room' }
|
|
console.log('Posting token to page... token length=', String(TOKEN).length)
|
|
|
|
// Attempt to post into iframe if present, else to window
|
|
await page.evaluate((payload) => {
|
|
try {
|
|
const iframe = document.querySelector('iframe#studio-portal')
|
|
if (iframe && iframe.contentWindow) {
|
|
iframe.contentWindow.postMessage(payload, '*')
|
|
window.__e2e_posted = 'iframe'
|
|
} else {
|
|
window.postMessage(payload, '*')
|
|
window.__e2e_posted = 'self'
|
|
}
|
|
} catch (e) {
|
|
try { window.postMessage(payload, '*'); window.__e2e_posted = 'self' } catch(_) { window.__e2e_posted = 'failed' }
|
|
}
|
|
}, payload)
|
|
|
|
// Wait for StudioPortal to show token received or show error/connected
|
|
const start = Date.now()
|
|
const timeout = 25000
|
|
let state = { status: 'unknown' }
|
|
while (Date.now() - start < timeout) {
|
|
const r = await page.evaluate(() => {
|
|
const out = { tokenFlag: !!(window as any).__e2e_posted, tokenTarget: (window as any).__e2e_posted || null }
|
|
try {
|
|
const tokenNotice = Array.from(document.querySelectorAll('div, span'))
|
|
.map(n => (n.textContent||'').toLowerCase())
|
|
.find(t => t.includes('token recibido') || t.includes('esperando token') || t.includes('token'))
|
|
if (tokenNotice) out.tokenNotice = tokenNotice
|
|
} catch(e) {}
|
|
// detect studio connected element (common selectors)
|
|
const connected = !!document.querySelector('.studio-status[data-status="connected"]')
|
|
if (connected) out.connected = true
|
|
// detect error modal
|
|
const err = document.querySelector('.studio-error-modal')
|
|
if (err) out.error = (err.textContent||'').trim()
|
|
return out
|
|
})
|
|
if (r.connected) { state = { status: 'connected', detail: r }; break }
|
|
if (r.error) { state = { status: 'error', detail: r }; break }
|
|
if (r.tokenFlag) state = { status: 'posted', detail: r }
|
|
await page.waitForTimeout(600)
|
|
}
|
|
|
|
const shotAfter = path.join(OUT, 'debug-after.png')
|
|
await page.screenshot({ path: shotAfter, fullPage: true })
|
|
console.log('Saved screenshot', shotAfter)
|
|
|
|
const outPath = path.join(OUT, 'debug-post-result.json')
|
|
await fs.promises.writeFile(outPath, JSON.stringify({ state, timestamp: Date.now() }, null, 2), 'utf8')
|
|
console.log('Wrote result to', outPath)
|
|
console.log('STATE:', JSON.stringify(state, null, 2))
|
|
|
|
await browser.close()
|
|
return state
|
|
} catch (err) {
|
|
console.error('Runner error', err && err.stack ? err.stack : err)
|
|
try { await browser.close() } catch(e) {}
|
|
process.exit(3)
|
|
}
|
|
}
|
|
|
|
run().catch(e=>{ console.error(e); process.exit(5) })
|