import { test, expect } from '@playwright/test'; import { chromium as pwChromium } from 'playwright'; // Defaults point to public deployments (overrideable via env) const BACKEND = process.env.TOKEN_SERVER || process.env.BACKEND || 'https://avanzacast-servertokens.bfzqqk.easypanel.host'; const BROADCAST = process.env.BROADCAST_URL || 'https://avanzacast-broadcastpanel.bfzqqk.easypanel.host'; const ROOM = process.env.ROOM || `e2e-visual-${Date.now()}`; const BROWSERLESS_WS = process.env.BROWSERLESS_WS || process.env.REMOTE_WS || ''; const BROWSERLESS_TOKEN = process.env.BROWSERLESS_TOKEN || process.env.BROWSERLESS_KEY || ''; async function createSession() { const res = await fetch(`${BACKEND.replace(/\/$/, '')}/api/session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ room: ROOM, username: 'visual-runner', ttl: 300 }), }); const text = await res.text(); try { return JSON.parse(text); } catch (e) { return null; } } async function resolveRemoteWSEndpoint(raw: string | undefined) { if (!raw) return null; let r = String(raw).trim(); // if starts with ws or wss, return as-is (may still need token appended) if (r.startsWith('ws://') || r.startsWith('wss://')) return r; // numeric port -> assume localhost if (/^\d+$/.test(r)) r = `http://localhost:${r}`; if (!r.startsWith('http://') && !r.startsWith('https://')) r = `http://${r}`; try { const ver = await fetch(r.replace(/\/$/, '') + '/json/version'); if (ver && ver.ok) { const j = await ver.json(); if (j.webSocketDebuggerUrl) return j.webSocketDebuggerUrl; } } catch (e) { /* ignore */ } try { const list = await fetch(r.replace(/\/$/, '') + '/json/list'); if (list && list.ok) { const arr = await list.json(); if (Array.isArray(arr) && arr.length && arr[0].webSocketDebuggerUrl) return arr[0].webSocketDebuggerUrl; } } catch (e) { /* ignore */ } try { const j = await fetch(r.replace(/\/$/, '') + '/json'); if (j && j.ok) { const arr = await j.json(); if (Array.isArray(arr) && arr.length && arr[0].webSocketDebuggerUrl) return arr[0].webSocketDebuggerUrl; } } catch (e) { /* ignore */ } return null; } test('Studio visual snapshot (browserless-aware)', async () => { const session = await createSession(); const target = session && session.studioUrl ? session.studioUrl : BROADCAST; // Create browser/page either via browserless CDP or local chromium let browser: any = null; let context: any = null; let page: any = null; let usedRemote = false; try { if (BROWSERLESS_WS) { // try resolve to webSocketDebuggerUrl (CDP) let connectEndpoint: string | null = null; // if BROWSERLESS_WS already contains token param, use as-is; otherwise append token if ((BROWSERLESS_WS.startsWith('ws://') || BROWSERLESS_WS.startsWith('wss://')) && BROWSERLESS_TOKEN) { connectEndpoint = `${BROWSERLESS_WS}${BROWSERLESS_WS.includes('?') ? '&' : '?'}token=${encodeURIComponent(BROWSERLESS_TOKEN)}`; } else { // try to resolve via http endpoints connectEndpoint = await resolveRemoteWSEndpoint(BROWSERLESS_WS + (BROWSERLESS_TOKEN ? (BROWSERLESS_WS.includes('?') ? '&' : '?') + `token=${encodeURIComponent(BROWSERLESS_TOKEN)}` : '')); if (!connectEndpoint) connectEndpoint = await resolveRemoteWSEndpoint(BROWSERLESS_WS); } if (connectEndpoint) { // connect via CDP try { browser = await pwChromium.connectOverCDP(connectEndpoint, { timeout: 20000 }); context = await browser.newContext({ viewport: { width: 1280, height: 800 } }); page = await context.newPage(); usedRemote = true; console.log('Connected to remote browser via CDP:', connectEndpoint); } catch (err) { console.warn('connectOverCDP failed, will fallback to local chromium:', String(err)); } } else { console.warn('Could not resolve remote CDP endpoint for', BROWSERLESS_WS); } } if (!page) { // fallback: launch local chromium browser = await pwChromium.launch({ headless: true }); context = await browser.newContext({ viewport: { width: 1280, height: 800 } }); page = await context.newPage(); console.log('Launched local chromium for visual test'); } console.log('Navigating to', target); await page.goto(target, { waitUntil: 'networkidle' }); // Wait a bit for UI to settle await page.waitForTimeout(1500); // Attempt to post message if we have token if (session && session.token) { try { await page.evaluate((tk) => { window.postMessage({ type: 'LIVEKIT_TOKEN', token: tk, room: '', url: window.location.href }, window.location.origin); }, session.token); } catch (e) { console.warn('postMessage failed', e); } } // Wait for connection indicators or token received text await page.waitForTimeout(1500); // Take screenshot and compare to baseline const now = Date.now(); const outDir = `${process.cwd()}/e2e/out/visual_${now}`; const fs = require('fs'); try { fs.mkdirSync(outDir, { recursive: true }); } catch (e) {} const capturePath = `${outDir}/studio.png`; await page.screenshot({ path: capturePath, fullPage: false }); console.log('Saved visual capture to', capturePath); // Playwright snapshot assertion (baseline management) await expect(page).toHaveScreenshot('studio.png', { fullPage: false }); } finally { try { if (context) await context.close(); } catch (e) {} try { if (browser && usedRemote && typeof browser.close === 'function') await browser.close(); else if (browser) await browser.close(); } catch (e) {} } });