- 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
205 lines
7.7 KiB
JavaScript
205 lines
7.7 KiB
JavaScript
// scripts/e2e/auto_enter_and_wait_token.js
|
|
// Automate navigation to broadcast-panel and repeatedly click "Entrar al estudio"
|
|
// until a token-like value appears in sessionStorage (key contains 'token' or 'livekit').
|
|
// Usage:
|
|
// CHROME_DEBUG_URL=http://127.0.0.1:9222/json/version E2E_LOG=/tmp/e2e_auto_run.log node scripts/e2e/auto_enter_and_wait_token.js [targetUrl] [--keep-open]
|
|
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const TARGET = process.argv.find(a => a && !a.startsWith('--')) || 'http://localhost:5175'
|
|
const KEEP_OPEN = process.argv.includes('--keep-open') || process.env.KEEP_OPEN === '1'
|
|
const E2E_LOG = process.env.E2E_LOG || '/tmp/e2e_auto_run.log'
|
|
|
|
function log(...args) {
|
|
try { fs.appendFileSync(E2E_LOG, `[${new Date().toISOString()}] ${args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}\n`) } catch (e){}
|
|
console.log(...args)
|
|
}
|
|
|
|
async function fetchJson(url) {
|
|
const fetch = global.fetch || (await import('node-fetch')).then(m => m.default)
|
|
const r = await fetch(url)
|
|
if (!r.ok) throw new Error(`fetch ${url} failed ${r.status}`)
|
|
return r.json()
|
|
}
|
|
|
|
async function findDebuggerWS(debugUrl) {
|
|
const info = await fetchJson(debugUrl)
|
|
return info.webSocketDebuggerUrl
|
|
}
|
|
|
|
async function findOrOpenPage(browser, targetUrl) {
|
|
const pages = await browser.pages()
|
|
for (const p of pages) {
|
|
try {
|
|
const u = p.url()
|
|
if (!u || u === 'about:blank') continue
|
|
if (u === targetUrl || u.startsWith(targetUrl) || u.includes('localhost:5175')) {
|
|
return p
|
|
}
|
|
} catch (e) { }
|
|
}
|
|
const p = await browser.newPage()
|
|
await p.bringToFront()
|
|
await p.goto(targetUrl, { waitUntil: 'networkidle2', timeout: 30000 })
|
|
return p
|
|
}
|
|
|
|
async function waitForXPathPoly(page, xpath, timeout = 15000, interval = 250) {
|
|
const start = Date.now()
|
|
while (Date.now() - start < timeout) {
|
|
try {
|
|
const els = await page.$x(xpath)
|
|
if (els && els.length) return els
|
|
} catch (e) {}
|
|
await new Promise(r => setTimeout(r, interval))
|
|
}
|
|
throw new Error(`Timeout waiting for XPath: ${xpath}`)
|
|
}
|
|
|
|
async function detectTokenInSession(page) {
|
|
return await page.evaluate(() => {
|
|
try {
|
|
const keys = Object.keys(sessionStorage || {})
|
|
for (const k of keys) {
|
|
const v = sessionStorage.getItem(k)
|
|
if (!v) continue
|
|
const kl = k.toLowerCase()
|
|
if (kl.includes('token') || kl.includes('livekit') || kl.includes('session') || kl.includes('studio')) {
|
|
if (typeof v === 'string' && v.length > 8) return { key: k, value: v }
|
|
}
|
|
}
|
|
// fallback: find any value that looks like JWT (three parts separated by .)
|
|
for (const k of keys) {
|
|
const v = sessionStorage.getItem(k) || ''
|
|
if (typeof v === 'string' && v.split('.').length === 3 && v.length > 20) return { key: k, value: v }
|
|
}
|
|
return null
|
|
} catch (e) { return null }
|
|
})
|
|
}
|
|
|
|
async function clickEnterStudio(page) {
|
|
// Try to click an 'Entrar al estudio' button; return true if clicked
|
|
const xpaths = [
|
|
"(//button[contains(normalize-space(.),'Entrar al estudio') or contains(@aria-label,'Entrar al estudio') or normalize-space(text())='Entrar al estudio'])[1]",
|
|
"//button[contains(.,'Entrar al estudio')][1]",
|
|
"//a[contains(.,'Entrar al estudio')][1]"
|
|
]
|
|
for (const xp of xpaths) {
|
|
try {
|
|
const els = await page.$x(xp)
|
|
if (els && els.length) {
|
|
await els[0].click()
|
|
return true
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
return false
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
const debugUrl = process.env.CHROME_DEBUG_URL || 'http://127.0.0.1:9222/json/version'
|
|
log('[auto] debugUrl', debugUrl, 'target', TARGET, 'KEEP_OPEN', KEEP_OPEN)
|
|
const ws = await findDebuggerWS(debugUrl)
|
|
log('[auto] ws', ws)
|
|
const puppeteer = require('puppeteer-core')
|
|
const browser = await puppeteer.connect({ browserWSEndpoint: ws, defaultViewport: { width: 1280, height: 800 } })
|
|
const page = await findOrOpenPage(browser, TARGET)
|
|
await page.bringToFront()
|
|
|
|
// install lightweight listeners to log relevant console messages
|
|
page.on('console', msg => {
|
|
try {
|
|
const txt = msg.text && typeof msg.text === 'function' ? msg.text() : ''
|
|
if (txt && (txt.startsWith('__E2E_') || /Token recibido|Esperando token|LIVEKIT|LIVEKIT_TOKEN|Entrar al estudio|StudioPortal|useStudioLauncher/.test(txt))) log('[page]', txt)
|
|
} catch (e) {}
|
|
})
|
|
|
|
log('[auto] starting loop to click Entrar al estudio and wait for session token')
|
|
const maxAttempts = 40
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
log('[auto] attempt', i+1)
|
|
// try to click enter studio
|
|
const clicked = await clickEnterStudio(page)
|
|
log('[auto] clickedEnter?', clicked)
|
|
|
|
// after clicking, wait a bit for overlay and for sessionStorage to update
|
|
try {
|
|
await page.waitForTimeout(1000)
|
|
// bring to front
|
|
try { await page.bringToFront() } catch(e){}
|
|
// check sessionStorage repeatedly for a short window
|
|
const checkStart = Date.now()
|
|
while (Date.now() - checkStart < 8000) {
|
|
const found = await detectTokenInSession(page)
|
|
if (found) {
|
|
log('[auto] TOKEN FOUND', found.key, 'len', (found.value || '').length)
|
|
// save screenshot and exit
|
|
const out = path.resolve(process.cwd(), 'auto_token_found.png')
|
|
await page.screenshot({ path: out, fullPage: true })
|
|
log('[auto] screenshot saved', out)
|
|
if (!KEEP_OPEN) {
|
|
await page.close()
|
|
await browser.disconnect()
|
|
process.exit(0)
|
|
} else {
|
|
log('[auto] KEEP_OPEN set; leaving page open. Ctrl+C to terminate.')
|
|
await new Promise(()=>{})
|
|
}
|
|
}
|
|
await page.waitForTimeout(500)
|
|
}
|
|
} catch (e) {
|
|
log('[auto] wait/check error', e && e.message)
|
|
}
|
|
|
|
// if not found, try to recreate the flow: click create -> skip -> start
|
|
try {
|
|
// try to click create transmission (if exists)
|
|
const createXPath = "//button[.//span/text() = 'Transmisión en vivo' or .//span[contains(text(),'Transmisión en vivo')]]"
|
|
try {
|
|
const createEls = await page.$x(createXPath)
|
|
if (createEls && createEls.length) { await createEls[0].click(); log('[auto] clicked create') }
|
|
} catch (e) {}
|
|
|
|
await page.waitForTimeout(500)
|
|
// try skip
|
|
try {
|
|
const [skip] = await page.$x("//*[normalize-space(text())='Omitir por ahora' or normalize-space(text())='Omitar por ahora' or normalize-space(text())='Skip for now']")
|
|
if (skip) { await skip.click(); log('[auto] clicked skip') }
|
|
} catch(e) {}
|
|
|
|
await page.waitForTimeout(300)
|
|
// try start
|
|
try {
|
|
const [startBtn] = await page.$x("//button[normalize-space(text())='Empezar ahora' or normalize-space(text())='Crear transmisión en vivo' or normalize-space(text())='Start now']")
|
|
if (startBtn) { await startBtn.click(); log('[auto] clicked start') }
|
|
} catch (e) {}
|
|
|
|
await page.waitForTimeout(800)
|
|
} catch (e) { log('[auto] flow recreate err', e && e.message) }
|
|
}
|
|
|
|
log('[auto] max attempts reached; did not find token')
|
|
const out = path.resolve(process.cwd(), 'auto_token_notfound.png')
|
|
await page.screenshot({ path: out, fullPage: true })
|
|
log('[auto] screenshot saved', out)
|
|
if (!KEEP_OPEN) {
|
|
await page.close()
|
|
await browser.disconnect()
|
|
process.exit(2)
|
|
} else {
|
|
log('[auto] KEEP_OPEN set; leaving page open. Ctrl+C to terminate.')
|
|
await new Promise(()=>{})
|
|
}
|
|
} catch (err) {
|
|
log('[auto] ERROR', err && (err.stack || err.message || String(err)))
|
|
process.exit(2)
|
|
}
|
|
}
|
|
|
|
main()
|
|
|