// e2e/validate-flow-remote-chrome.js // Connect to a remote Chrome (remote debugging port 9222) and run the Broadcast->Studio E2E flow. // Usage: // CHROME_WS (optional) = full websocketDebuggerUrl // CHROME_HOST (optional) = host:port (default port 9222) e.g. 1.2.3.4 or 1.2.3.4:9222 // BROADCAST_URL (required) - broadcast panel URL // STUDIO_URL (optional) // TOKEN (optional) - token to append when forcing studio navigation const fs = require('fs'); const path = require('path'); const fetch = require('node-fetch'); const puppeteer = require('puppeteer-core'); async function getWsEndpointFromHost(host) { // host maybe '1.2.3.4' or '1.2.3.4:9222' const url = host.includes(':') ? `http://${host}/json/version` : `http://${host}:9222/json/version`; const res = await fetch(url, { timeout: 5000 }).catch(err => { throw new Error(`Failed to fetch ${url}: ${err.message}`); }); if (!res.ok) throw new Error(`Failed to get json/version from ${url}: status=${res.status}`); const json = await res.json(); if (!json.webSocketDebuggerUrl) throw new Error(`No webSocketDebuggerUrl in ${url} response`); return json.webSocketDebuggerUrl; } (async () => { const outDir = path.resolve(__dirname); const results = { startedAt: new Date().toISOString(), console: [], navigations: [] }; const CHROME_WS = process.env.CHROME_WS || null; const CHROME_HOST = process.env.CHROME_HOST || process.env.CHROME_REMOTE || null; // host[:port] let wsEndpoint = CHROME_WS; try { if (!wsEndpoint) { if (!CHROME_HOST) throw new Error('CHROME_WS or CHROME_HOST required (host[:port] or ws url)'); wsEndpoint = await getWsEndpointFromHost(CHROME_HOST); } } catch (err) { console.error('Failed to determine Chrome websocket endpoint:', err.message || err); process.exit(2); } console.log('Using Chrome websocket endpoint:', wsEndpoint); const BROADCAST_URL = process.env.BROADCAST_URL || process.env.VITE_BROADCASTPANEL_URL || null; const STUDIO_URL = process.env.STUDIO_URL || process.env.VITE_STUDIO_URL || null; const TOKEN = process.env.TOKEN || process.env.E2E_TOKEN || ''; if (!BROADCAST_URL) { console.error('BROADCAST_URL is required'); process.exit(2); } let browser; try { browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint, ignoreHTTPSErrors: true, defaultViewport: { width: 1366, height: 768 } }); } catch (err) { console.error('Failed to connect to remote Chrome:', err && err.message ? err.message : err); process.exit(2); } 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(1500); // Try to find a button/link to enter the studio const texts = ['Entrar al estudio', 'Entrar al Studio', 'Entrar al Estudio', 'Enter studio', 'Enter the studio']; let studioOpened = false; for (const t of texts) { const els = await page.$x(`//a[contains(normalize-space(.), '${t}')] | //button[contains(normalize-space(.), '${t}')] | //span[contains(normalize-space(.), '${t}')]`); if (els && els.length) { console.log('Found element for text:', t, 'count=', els.length); try { // prepare for new target const popupPromise = new Promise(resolve => { const onTarget = target => resolve(target); browser.once('targetcreated', onTarget); setTimeout(() => { try { browser.removeListener('targetcreated', onTarget); } catch(e){}; resolve(null); }, 4000); }); await els[0].click({ delay: 50 }).catch(() => null); const popupTarget = await popupPromise; let studioPage = null; if (popupTarget) { try { studioPage = await popupTarget.page(); } catch (e) { studioPage = null; } } if (!studioPage) { await page.waitForTimeout(800); const url = page.url(); if (url.includes('/studio') || url.includes('studio')) studioPage = page; } if (studioPage) { console.log('Studio page found; navigating with token...'); // Prefer STUDIO_URL if provided; otherwise use BROADCAST_URL (strip trailing slash) as entry with ?token const broadcastBase = (BROADCAST_URL || '').replace(/\/$/, ''); const targetStudioUrl = STUDIO_URL && STUDIO_URL.length ? `${STUDIO_URL}?token=${encodeURIComponent(TOKEN)}` : `${broadcastBase}?token=${encodeURIComponent(TOKEN)}`; await studioPage.goto(targetStudioUrl, { waitUntil: 'networkidle2' }); results.navigations.push({ type: 'studio_opened', url: studioPage.url() }); await studioPage.waitForTimeout(2500); // --- ASSERTIONS: check sessionStorage on broadcast page and content on studio page results.assertions = results.assertions || []; try { const storeKey = 'avanzacast_studio_session'; const stored = await page.evaluate((k) => { try { return sessionStorage.getItem(k); } catch(e) { return null; } }, storeKey); if (stored) { try { const parsed = JSON.parse(stored); // Accept any token returned by backend; if TOKEN env is set we also note if it matches if (parsed && parsed.token) { const note = (typeof TOKEN === 'string' && TOKEN.length && String(parsed.token).includes(TOKEN.slice(0,6))) ? 'sessionStorage contains token (matches provided TOKEN prefix)' : 'sessionStorage contains token (from backend)'; results.assertions.push({ name: 'sessionStorage_has_token', ok: true, detail: note }); } else { results.assertions.push({ name: 'sessionStorage_has_token', ok: false, detail: 'sessionStorage present but token missing or malformed' }); } } catch(e) { results.assertions.push({ name: 'sessionStorage_parse', ok: false, detail: 'sessionStorage JSON parse failed: ' + String(e) }); } } else { results.assertions.push({ name: 'sessionStorage_has_token', ok: false, detail: 'sessionStorage key not found' }); } } catch (e) { results.assertions.push({ name: 'sessionStorage_check_failed', ok: false, detail: String(e) }); } try { const bodyText = await studioPage.evaluate(() => (document.body && document.body.innerText) ? document.body.innerText : ''); if (bodyText.includes('token=') || (TOKEN && bodyText.includes(TOKEN.slice(0,8)))) { results.assertions.push({ name: 'studio_page_shows_token', ok: true, detail: 'Studio page contains token or token prefix' }); } else { results.assertions.push({ name: 'studio_page_shows_token', ok: false, detail: 'Studio page does not show token text' }); } } catch (e) { results.assertions.push({ name: 'studio_page_check_failed', ok: false, detail: String(e) }); } const shot = path.join(outDir, 'validate-remote-chrome-result.png'); await studioPage.screenshot({ path: shot, fullPage: true }); results.screenshot = shot; studioOpened = true; break; } } catch (err) { console.warn('Click attempt error', err && err.message); } } } if (!studioOpened) { // fallback: navigate directly to studio with token 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, 'validate-remote-chrome-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, 'validate-flow-remote-chrome-result.json'); fs.writeFileSync(outJson, JSON.stringify(results, null, 2)); console.log('Wrote results to', outJson); // append log and publish artifact try { const { publishArtifact, appendLog } = require('./logging'); const artifactUrl = publishArtifact(outJson, 'validate-flow-remote-chrome') || null; appendLog('validate-flow-remote-chrome', outJson, results, artifactUrl); } catch (e) { console.warn('Failed to write LOG.md entry', e); } try { await page.close(); } catch(e){} try { await browser.disconnect(); } catch(e) { try { await browser.close(); } catch(e){} } process.exit(0); } catch (err) { console.error('Error validating flow on remote chrome', 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, 'validate-flow-remote-chrome-result.json'), JSON.stringify(results, null, 2)); try { await page.close(); } catch(e){} try { await browser.disconnect(); } catch(e) { try { await browser.close(); } catch(e){} } process.exit(1); } })();