AvanzaCast/packages/broadcast-panel/e2e/visual-studio.spec.ts

133 lines
5.7 KiB
TypeScript

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) {}
}
});