- 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
163 lines
7.0 KiB
JavaScript
163 lines
7.0 KiB
JavaScript
// e2e/validate-flow-domains-local.js
|
|
// Local Puppeteer script to validate studio opening flow for AvanzaCast
|
|
// - Navigates to BROADCAST_URL (VITE_BROADCASTPANEL_URL)
|
|
// - Clicks 'Entrar al estudio' (or opens studio route)
|
|
// - Opens or navigates the Studio Portal with provided TOKEN and LIVEKIT WS URL
|
|
// - Captures logs and a screenshot and writes a result JSON
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const puppeteer = require('puppeteer');
|
|
|
|
(async () => {
|
|
const outDir = path.resolve(__dirname);
|
|
const results = { startedAt: new Date().toISOString(), console: [], navigations: [] };
|
|
|
|
const BROADCAST_URL = process.env.VITE_BROADCASTPANEL_URL || process.env.BROADCAST_URL;
|
|
const STUDIO_URL = process.env.VITE_STUDIO_URL || process.env.STUDIO_URL || null;
|
|
const TOKEN = process.env.TOKEN || process.env.E2E_TOKEN;
|
|
const LIVEKIT_WS = process.env.VITE_LIVEKIT_WS_URL || process.env.LIVEKIT_WS;
|
|
const TOKEN_SERVER = process.env.VITE_TOKEN_SERVER_URL || process.env.TOKEN_SERVER_URL;
|
|
const HEADLESS = process.env.HEADLESS === '0' ? false : (process.env.HEADLESS === '1' ? true : true);
|
|
|
|
console.log('HEADLESS:', HEADLESS);
|
|
|
|
if (!BROADCAST_URL) {
|
|
console.error('BROADCAST_URL (VITE_BROADCASTPANEL_URL) is required');
|
|
process.exit(2);
|
|
}
|
|
if (!TOKEN) {
|
|
console.error('TOKEN env is required (the e2e token to pass to studio)');
|
|
process.exit(2);
|
|
}
|
|
|
|
console.log('Broadcast URL:', BROADCAST_URL);
|
|
console.log('Studio URL:', STUDIO_URL || '(not provided)');
|
|
console.log('LiveKit WS:', LIVEKIT_WS || '(not provided)');
|
|
console.log('Token server:', TOKEN_SERVER || '(not provided)');
|
|
|
|
const browser = await puppeteer.launch({ headless: HEADLESS, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
|
|
const page = await browser.newPage();
|
|
page.setDefaultNavigationTimeout(30000);
|
|
|
|
page.on('console', msg => {
|
|
try { results.console.push({ type: msg.type(), text: msg.text() }); } catch(e){}
|
|
});
|
|
page.on('pageerror', err => { results.console.push({ type: 'pageerror', text: String(err && err.stack ? err.stack : err) }); });
|
|
|
|
try {
|
|
await page.goto(BROADCAST_URL, { waitUntil: 'networkidle2' });
|
|
await page.waitForTimeout(1200);
|
|
|
|
// Try to find a button or link with text Entrar al estudio or Enter studio
|
|
const texts = ['Entrar al estudio', 'Entrar al Studio', 'Enter studio', 'Enter the studio', 'Entrar al estudio'];
|
|
let clicked = false;
|
|
for (const t of texts) {
|
|
const els = await page.$x(`//a[contains(normalize-space(.), '${t}')] | //button[contains(normalize-space(.), '${t}')]`);
|
|
if (els && els.length) {
|
|
console.log('Found element for text:', t);
|
|
try {
|
|
// attempt to click and wait for popup or navigation
|
|
const popupPromise = new Promise(resolve => {
|
|
const onTarget = target => {
|
|
try { resolve(target); } catch (e) { resolve(null); }
|
|
};
|
|
browser.once('targetcreated', onTarget);
|
|
// safety timeout
|
|
setTimeout(() => { browser.removeListener('targetcreated', onTarget); resolve(null); }, 3000);
|
|
});
|
|
|
|
await els[0].click({ delay: 50 }).catch(() => null);
|
|
const popupTarget = await popupPromise;
|
|
|
|
// Check for new target (popup)
|
|
let studioPage = null;
|
|
if (popupTarget) {
|
|
try {
|
|
studioPage = await popupTarget.page();
|
|
} catch(e) { studioPage = null; }
|
|
}
|
|
|
|
// If no popup, maybe navigation in same page
|
|
if (!studioPage) {
|
|
// Wait for navigation or a url change indicating studio
|
|
await page.waitForTimeout(800);
|
|
const url = page.url();
|
|
if (url.includes('/studio') || url.includes('studio')) {
|
|
studioPage = page;
|
|
}
|
|
}
|
|
|
|
// If studioPage found and we have STUDIO_URL, force navigate with token
|
|
if (studioPage) {
|
|
console.log('Studio page found; navigating with token...');
|
|
const targetStudioUrl = STUDIO_URL ? `${STUDIO_URL}?token=${encodeURIComponent(TOKEN)}` : `${page.url().split('?')[0]}?token=${encodeURIComponent(TOKEN)}`;
|
|
await studioPage.goto(targetStudioUrl, { waitUntil: 'networkidle2' });
|
|
results.navigations.push({ type: 'studio_opened', url: studioPage.url() });
|
|
// Wait a bit for LiveKit connect attempts (if any)
|
|
await studioPage.waitForTimeout(2500);
|
|
const shot = path.join(outDir, 'studio_flow_result.png');
|
|
await studioPage.screenshot({ path: shot, fullPage: true });
|
|
results.screenshot = shot;
|
|
clicked = true;
|
|
break;
|
|
} else {
|
|
console.log('Click did not open studio; will try next match');
|
|
}
|
|
} catch (err) {
|
|
console.warn('Click attempt error', err && err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!clicked) {
|
|
// Fallback: try a generic selector or direct navigation
|
|
const altSel = 'a#enter-studio, button#enter-studio, a[data-enter-studio]';
|
|
try {
|
|
const alt = await page.$(altSel);
|
|
if (alt) {
|
|
console.log('Found alternative selector, clicking...');
|
|
await alt.click().catch(() => null);
|
|
await page.waitForTimeout(1000);
|
|
}
|
|
} catch(e){}
|
|
|
|
// Fallback: navigate directly to studio with token if STUDIO_URL provided
|
|
if (STUDIO_URL) {
|
|
console.log('Fallback: navigating directly to STUDIO_URL with token');
|
|
const directUrl = `${STUDIO_URL}?token=${encodeURIComponent(TOKEN)}`;
|
|
await page.goto(directUrl, { waitUntil: 'networkidle2' });
|
|
await page.waitForTimeout(1500);
|
|
const shot = path.join(outDir, 'studio_flow_result.png');
|
|
await page.screenshot({ path: shot, fullPage: true });
|
|
results.screenshot = shot;
|
|
results.navigations.push({ type: 'direct_studio', url: directUrl });
|
|
} else {
|
|
console.error('No studio opened and no STUDIO_URL provided for fallback.');
|
|
}
|
|
}
|
|
|
|
results.endedAt = new Date().toISOString();
|
|
const outJson = path.join(outDir, 'studio-flow-domains-result.json');
|
|
fs.writeFileSync(outJson, JSON.stringify(results, null, 2));
|
|
console.log('Wrote results to', outJson);
|
|
|
|
// Publish artifact (JSON + screenshot) and append run summary to LOG.md
|
|
try {
|
|
const { publishArtifact, appendLog } = require('./logging');
|
|
const artifactUrl = publishArtifact(outJson, 'validate-flow-domains-local') || null;
|
|
appendLog('validate-flow-domains-local', outJson, results, artifactUrl);
|
|
} catch (e) { console.warn('Failed to publish/appenda log entry', e); }
|
|
|
|
await browser.close();
|
|
process.exit(0);
|
|
} catch (err) {
|
|
console.error('Error validating flow', err && err.stack ? err.stack : err);
|
|
results.error = String(err && err.stack ? err.stack : err);
|
|
results.endedAt = new Date().toISOString();
|
|
fs.writeFileSync(path.join(outDir, 'studio-flow-domains-result.json'), JSON.stringify(results, null, 2));
|
|
try { await browser.close(); } catch(e){}
|
|
process.exit(1);
|
|
}
|
|
})();
|