- 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
212 lines
8.4 KiB
JavaScript
212 lines
8.4 KiB
JavaScript
// filepath: packages/broadcast-panel/e2e/run_browserless_e2e.js
|
|
import fetch from 'node-fetch';
|
|
import puppeteer from 'puppeteer-core';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
async function main() {
|
|
const BROWSERLESS_WS = process.env.BROWSERLESS_WS || process.env.BROWSERLESS_URL || 'wss://browserless.bfzqqk.easypanel.host';
|
|
const BROWSERLESS_TOKEN = process.env.BROWSERLESS_TOKEN || process.env.BROWSERLESS_KEY || '';
|
|
const TOKEN_SERVER = process.env.TOKEN_SERVER || 'https://avanzacast-servertokens.bfzqqk.easypanel.host';
|
|
const ROOM = process.env.ROOM || 'e2e-room';
|
|
const USERNAME = process.env.USERNAME || 'e2e-runner';
|
|
const OUT_DIR = process.env.OUT_DIR || null;
|
|
|
|
function outLog(...args) {
|
|
console.log(...args);
|
|
if (OUT_DIR) {
|
|
try {
|
|
fs.appendFileSync(path.join(OUT_DIR, 'e2e.log'), args.map(String).join(' ') + '\n');
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
}
|
|
|
|
outLog('E2E runner starting with:', { BROWSERLESS_WS, TOKEN_SERVER, ROOM, USERNAME, OUT_DIR });
|
|
|
|
if (!BROWSERLESS_TOKEN) {
|
|
outLog('Missing BROWSERLESS_TOKEN env');
|
|
process.exit(2);
|
|
}
|
|
|
|
outLog('Creating session on token server', TOKEN_SERVER, ROOM, USERNAME);
|
|
let resp;
|
|
try {
|
|
resp = await fetch(`${TOKEN_SERVER.replace(/\/$/, '')}/api/session`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ room: ROOM, username: USERNAME, ttl: 300 }),
|
|
});
|
|
} catch (err) {
|
|
outLog('Network error while calling token server:', String(err));
|
|
process.exit(3);
|
|
}
|
|
|
|
outLog('Token server responded with status', resp.status, resp.statusText);
|
|
let data;
|
|
try {
|
|
const text = await resp.text();
|
|
try { data = JSON.parse(text); } catch(e) { data = null; }
|
|
outLog('Token server response body:', text);
|
|
} catch (err) {
|
|
outLog('Failed to read token server response body', String(err));
|
|
process.exit(3);
|
|
}
|
|
|
|
if (!resp.ok) {
|
|
outLog('Failed to create session, status', resp.status);
|
|
process.exit(4);
|
|
}
|
|
|
|
if (!data) {
|
|
outLog('Token server returned non-JSON or empty body');
|
|
process.exit(5);
|
|
}
|
|
|
|
outLog('Session created:', data);
|
|
const sessionId = data.id;
|
|
const studioUrl = data.studioUrl || data.redirectUrl || data.url;
|
|
let token = data.token || null;
|
|
|
|
if (!studioUrl) {
|
|
outLog('No studio URL returned from token server');
|
|
process.exit(4);
|
|
}
|
|
|
|
// If POST didn't return the token, try to GET it from the session endpoint
|
|
if (!token && sessionId) {
|
|
outLog('POST did not include token, attempting GET /api/session/:id to fetch token');
|
|
try {
|
|
const sessResp = await fetch(`${TOKEN_SERVER.replace(/\/$/, '')}/api/session/${encodeURIComponent(sessionId)}`);
|
|
const sessText = await sessResp.text();
|
|
let sessJson = null;
|
|
try { sessJson = JSON.parse(sessText); } catch(e) { sessJson = null; }
|
|
outLog('GET /api/session/:id status', sessResp.status, 'body start:', String(sessText).slice(0,400));
|
|
if (sessJson && sessJson.token) {
|
|
token = sessJson.token;
|
|
outLog('Obtained token from GET /api/session/:id (length', (token && token.length) || 0, ')');
|
|
} else {
|
|
outLog('GET /api/session/:id did not return token');
|
|
}
|
|
} catch (err) {
|
|
outLog('Error while GET /api/session/:id', String(err));
|
|
}
|
|
}
|
|
|
|
// connect to browserless
|
|
const wsEndpoint = `${BROWSERLESS_WS}${BROWSERLESS_WS.includes('?') ? '&' : '?'}token=${encodeURIComponent(BROWSERLESS_TOKEN)}`;
|
|
outLog('Connecting to browserless WS endpoint:', wsEndpoint);
|
|
let browser;
|
|
try {
|
|
browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint, defaultViewport: { width: 1366, height: 768 }, timeout: 20000 });
|
|
} catch (err) {
|
|
outLog('Failed to connect to browserless via puppeteer.connect:', err && err.stack ? err.stack : String(err));
|
|
process.exit(6);
|
|
}
|
|
|
|
try {
|
|
const page = await browser.newPage();
|
|
page.on('console', msg => {
|
|
try { outLog('[BROWSER]', msg.type(), msg.text()); } catch(e){}
|
|
});
|
|
page.on('pageerror', err => outLog('[PAGEERROR]', err && err.stack ? err.stack : String(err)));
|
|
|
|
// Log network failures
|
|
page.on('requestfailed', req => {
|
|
try { outLog('[REQFAILED]', req.url(), req.failure() && req.failure().errorText); } catch(e){}
|
|
});
|
|
page.on('response', async res => {
|
|
try {
|
|
const status = res.status();
|
|
if (status >= 400) {
|
|
outLog('[RESP_ERR]', status, res.url());
|
|
if (OUT_DIR) {
|
|
try {
|
|
const text = await res.text();
|
|
fs.writeFileSync(path.join(OUT_DIR, `resp_${sessionId || 'noid'}_${status}.txt`), text);
|
|
outLog('Saved failing response body to', path.join(OUT_DIR, `resp_${sessionId || 'noid'}_${status}.txt`));
|
|
} catch (e) { outLog('Failed to save response body', String(e)); }
|
|
}
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
});
|
|
|
|
outLog('Navigating to studioUrl:', studioUrl);
|
|
try {
|
|
await page.goto(studioUrl, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
} catch (err) {
|
|
outLog('page.goto failed:', err && err.stack ? err.stack : String(err));
|
|
throw err;
|
|
}
|
|
|
|
// Wait for StudioPortal text indicating waiting for token
|
|
const waited = await page.waitForFunction(() => {
|
|
return document.body && document.body.innerText && (document.body.innerText.includes('Esperando token') || document.body.innerText.includes('Token recibido'));
|
|
}, { timeout: 8000 }).catch(() => false);
|
|
|
|
if (!waited) outLog('Did not see StudioPortal waiting/received token text');
|
|
|
|
// If token was not included in redirect, try to postMessage token to window
|
|
if (token) {
|
|
outLog('Posting token via postMessage (token length', token.length, ')');
|
|
try {
|
|
await page.evaluate((tk) => {
|
|
window.postMessage({ type: 'LIVEKIT_TOKEN', token: tk, url: window.location.href, room: '' }, window.location.origin);
|
|
}, token);
|
|
} catch (err) {
|
|
outLog('postMessage evaluate failed:', err && err.stack ? err.stack : String(err));
|
|
}
|
|
} else {
|
|
outLog('No token present in session response; relying on redirect/session id flow');
|
|
}
|
|
|
|
// Wait for StudioPortal to report token received
|
|
const gotToken = await page.waitForFunction(() => {
|
|
return document.body && document.body.innerText && document.body.innerText.includes('Token recibido desde Broadcast Panel');
|
|
}, { timeout: 10000 }).catch(() => false);
|
|
|
|
if (gotToken) {
|
|
outLog('SUCCESS: StudioPortal received token via postMessage or redirect.');
|
|
} else {
|
|
outLog('FAIL: StudioPortal did not report token received within timeout.');
|
|
// print some page content for debugging
|
|
const snapshotText = await page.evaluate(() => document.body ? document.body.innerText.slice(0, 2000) : '');
|
|
outLog('Page snapshot:', snapshotText);
|
|
if (OUT_DIR) {
|
|
try {
|
|
const html = await page.content();
|
|
fs.writeFileSync(path.join(OUT_DIR, `page_${sessionId || 'noid'}.html`), html);
|
|
outLog('Saved full page HTML to', path.join(OUT_DIR, `page_${sessionId || 'noid'}.html`));
|
|
} catch (e) { outLog('Failed to save page HTML', String(e)); }
|
|
}
|
|
if (OUT_DIR) {
|
|
try {
|
|
await page.screenshot({ path: path.join(OUT_DIR, `e2e_${sessionId || 'noid'}.png`), fullPage: true });
|
|
outLog('Saved screenshot to', path.join(OUT_DIR, `e2e_${sessionId || 'noid'}.png`));
|
|
} catch (e) { outLog('Failed to save screenshot', e && e.stack ? e.stack : String(e)); }
|
|
}
|
|
process.exit(5);
|
|
}
|
|
|
|
// Optionally wait for connection attempt log
|
|
const connected = await page.waitForFunction(() => {
|
|
return document.body && document.body.innerText && document.body.innerText.includes('Conectado');
|
|
}, { timeout: 10000 }).catch(() => false);
|
|
|
|
outLog('Connected flag on StudioPortal:', !!connected);
|
|
outLog('E2E finished');
|
|
|
|
if (OUT_DIR) {
|
|
try {
|
|
await page.screenshot({ path: path.join(OUT_DIR, `e2e_${sessionId || 'noid'}.png`), fullPage: true });
|
|
outLog('Saved screenshot to', path.join(OUT_DIR, `e2e_${sessionId || 'noid'}.png`));
|
|
} catch (e) { outLog('Failed to save screenshot', e && e.stack ? e.stack : String(e)); }
|
|
}
|
|
|
|
await page.close();
|
|
} finally {
|
|
try { await browser.disconnect(); } catch(e){}
|
|
}
|
|
}
|
|
|
|
main().catch(err => { console.error('Unhandled error in main:', err && err.stack ? err.stack : String(err)); process.exit(99); });
|