AvanzaCast/packages/studio-panel/scripts/playwright_mcp_flow.mjs

286 lines
15 KiB
JavaScript

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const LOG = '/tmp/playwright_mcp_flow.log';
// ensure log file exists and add header
try{ fs.writeFileSync(LOG, `=== playwright_mcp_flow log started ${new Date().toISOString()} ===\n`); } catch(e) { console.error('Could not create log file', e); }
function log(msg){ const line = `[${new Date().toISOString()}] ${msg}\n`; try{ fs.appendFileSync(LOG, line);}catch(e){}; try{ console.log(msg);}catch(e){} }
console.log('playwright_mcp_flow: starting script');
log('script entry');
// Robust dynamic import of playwright — if missing, write to log and exit gracefully
let playwright;
try{ playwright = await import('playwright'); } catch (e) { fs.appendFileSync(LOG, `Playwright import error: ${e}\n`); console.error('Playwright not available. Install with `npm i -D playwright` and run `npx playwright install`'); process.exit(2); }
const { chromium } = playwright;
// If a remote Playwright server WS endpoint is provided, connect to it instead of launching locally.
const PLAYWRIGHT_WS = process.env.PLAYWRIGHT_WS_ENDPOINT || process.env.PW_WS || 'ws://192.168.1.20:3003';
let remoteBrowser = null;
let launchedBrowser = null;
(async ()=>{
log('Starting playwright_mcp_flow');
// Connect to remote server if reachable; else fall back to local launch
let browser;
try {
if (PLAYWRIGHT_WS) {
log('Attempting to connect to remote Playwright WS at ' + PLAYWRIGHT_WS);
try {
browser = await chromium.connect({ wsEndpoint: PLAYWRIGHT_WS, timeout: 10000 });
remoteBrowser = browser;
log('Connected to remote Playwright browser via WS');
} catch (err) {
// If server responded with 428 (version mismatch) surface clear message
const emsg = (err && err.message) ? String(err.message) : String(err);
if (emsg.includes('428') || emsg.toLowerCase().includes('version')) {
log('Failed to connect to remote Playwright WS due to version mismatch: ' + emsg);
log('ACTION REQUIRED: Sync Playwright versions. Server reports a different Playwright version than the client.');
} else {
log('Failed to connect to remote Playwright WS: ' + err + '. Falling back to local launch.');
}
}
}
} catch (e) { log('Remote connect error: '+e); }
if (!browser) {
log('Launching local chromium');
launchedBrowser = await chromium.launch({ headless: true, args: ['--no-sandbox','--disable-dev-shm-usage'] });
browser = launchedBrowser;
}
const context = await browser.newContext();
const page = await context.newPage();
page.on('console', m => log('PAGE LOG: ' + m.text()));
// Load selectors JSON
// convert module URL to file path correctly
const selPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../e2e/selectors_streamyard.json');
let SELECTORS = {};
try{
const raw = fs.readFileSync(selPath, 'utf8');
const parsed = JSON.parse(raw);
(parsed.selectors || []).forEach(s => { SELECTORS[s.id] = s; });
log(`Loaded ${Object.keys(SELECTORS).length} selectors from ${selPath}`);
}catch(err){ log('Failed to load selectors JSON: '+err); }
function getSel(id){
const s = SELECTORS[id];
if (!s) return null;
// prefer Playwright selector string if present
return s.playwright || s.css || s.xpath || null;
}
// Configurable env vars
const BROADCAST_URL = process.env.BROADCAST_URL || process.env.BROADCAST_LIST_URL || 'https://streamyard.com/broadcasts';
const BROADCAST_TIMEOUT = Number(process.env.BROADCAST_TIMEOUT_MS || 30000);
const STUDIO_TIMEOUT = Number(process.env.STUDIO_TIMEOUT_MS || 30000);
try{
log('Navigating to broadcast list: ' + BROADCAST_URL);
await page.goto(BROADCAST_URL, { waitUntil: 'networkidle', timeout: BROADCAST_TIMEOUT });
log('Broadcast page loaded: ' + page.url());
// If the broadcast page attempted to fetch a token but failed due to CORS, detect it in console logs
// We'll check page content for common CORS error markers and fallback to backend-api session creation.
const pageContent = await page.content().catch(()=>null);
const corsIssue = pageContent && (pageContent.includes('Failed to fetch') || pageContent.includes('blocked by CORS') || pageContent.includes('error%3A%20TypeError'));
if (corsIssue) {
log('Detected token fetch CORS issue on broadcast page — will create session via BACKEND_API_URL fallback');
// First attempt: try to fetch token directly from token server (server-side request)
const TOKEN_SERVER = process.env.TOKEN_SERVER_URL || process.env.VITE_TOKEN_SERVER_URL || 'https://avanzacast-servertokens.bfzqqk.easypanel.host';
try {
log('Attempting direct token request to token-server: ' + TOKEN_SERVER + '/api/token');
const room = process.env.TEST_ROOM || 'studio-demo';
const username = process.env.TEST_USERNAME || 'simulator';
const tokenResp = await context.request.get(TOKEN_SERVER + `/api/token?room=${encodeURIComponent(room)}&username=${encodeURIComponent(username)}`);
if (tokenResp && tokenResp.ok()) {
const json = await tokenResp.json();
log('Token server returned JSON: ' + JSON.stringify(Object.keys(json)));
const token = json.token || json?.token;
const serverUrl = json.url || json?.url || process.env.VITE_LIVEKIT_WS_URL || process.env.VITE_LIVEKIT_URL;
if (token) {
// compose studio URL
const studioBase = (process.env.VITE_STUDIO_URL || 'http://localhost:3020').replace(/\/$/, '');
const redirectUrl = `${studioBase}/studio_receiver.html?token=${encodeURIComponent(token)}&room=${encodeURIComponent(room)}&username=${encodeURIComponent(username)}&serverUrl=${encodeURIComponent(serverUrl||'')}`;
log('Opening studio directly with token from token-server: ' + redirectUrl);
const studioPage = await context.newPage();
await studioPage.goto(redirectUrl, { waitUntil: 'networkidle', timeout: 30000 }).catch(e=>log('studio goto failed: '+e));
page = studioPage; // eslint-disable-line
// skip backend fallback
corsIssue = false;
} else {
log('Token server response did not include token');
}
} else {
log('Token server request failed, status=' + (tokenResp && tokenResp.status()));
}
} catch (e) {
log('Direct token-server request failed: ' + e);
}
const BACKEND_API = process.env.BACKEND_API_URL || 'http://localhost:4000';
try {
log('Creating session via backend API: ' + BACKEND_API + '/api/session');
// use Playwright's request to perform POST (works with remote browser)
const room = process.env.TEST_ROOM || 'studio-demo';
const username = process.env.TEST_USERNAME || 'simulator';
const ttl = Number(process.env.TEST_TTL || 300);
const resp = await context.request.post(BACKEND_API + '/api/session', {
data: { room, username, ttl }
});
if (resp && resp.status() === 200) {
const json = await resp.json();
log('Session created: ' + JSON.stringify(json));
const redirectUrl = json.redirectUrl || json.studioUrl;
if (redirectUrl) {
log('Opening studio redirectUrl from backend session: ' + redirectUrl);
// open new page for studio
const studioPage = await context.newPage();
await studioPage.goto(redirectUrl, { waitUntil: 'networkidle', timeout: 30000 }).catch(e=>log('studio goto failed: '+e));
// replace studioPage variable used below
page = studioPage; // eslint-disable-line
} else {
log('Backend session response did not include redirectUrl/studioUrl');
}
} else {
log('Backend session creation failed, status=' + (resp && resp.status()));
}
} catch (e) {
log('Error creating session via backend API fallback: ' + e);
}
}
// --- Login handling: if redirected to /login or login form present, attempt automated login using env vars
try {
const maybeLogin = page.url().includes('/login');
// also detect common login form inputs
const hasEmailInput = await page.locator('input[type="email"], input[name="email"], input[id*=email]').count();
const hasPasswordInput = await page.locator('input[type="password"], input[name="password"], input[id*=password]').count();
if (maybeLogin || hasEmailInput || hasPasswordInput) {
const TEST_USER_EMAIL = process.env.TEST_USER_EMAIL || process.env.STREAMYARD_TEST_EMAIL || '';
const TEST_USER_PASSWORD = process.env.TEST_USER_PASSWORD || process.env.STREAMYARD_TEST_PASSWORD || '';
log('Detected login page/form; env email present? ' + (TEST_USER_EMAIL ? 'yes' : 'no'));
if (TEST_USER_EMAIL && TEST_USER_PASSWORD) {
// fill email
const emailSelectors = ['input[type="email"]','input[name="email"]','input[id*=email]'];
for (const sel of emailSelectors) {
try { if (await page.locator(sel).count()) { await page.fill(sel, TEST_USER_EMAIL); log('Filled email using selector: '+sel); break; } } catch(e){}
}
// fill password
const passSelectors = ['input[type="password"]','input[name="password"]','input[id*=password]'];
for (const sel of passSelectors) {
try { if (await page.locator(sel).count()) { await page.fill(sel, TEST_USER_PASSWORD); log('Filled password using selector: '+sel); break; } } catch(e){}
}
// try submit with several button selectors
const submitSelectors = [
'button:has-text("Iniciar sesión")',
'button:has-text("Iniciar Sesión")',
'button:has-text("Sign in")',
'button[type="submit"]'
];
let clicked = false;
for (const s of submitSelectors) {
try {
if (await page.locator(s).count()) { await page.click(s); log('Clicked submit using selector: '+s); clicked = true; break; }
} catch(e){}
}
// if no button clicked, try pressing Enter on password field
if (!clicked) {
try { await page.keyboard.press('Enter'); log('Pressed Enter to submit login form'); } catch(e){}
}
// wait for navigation away from /login or for broadcasts path
try {
await page.waitForFunction(() => !location.pathname.includes('/login'), { timeout: 20000 });
log('Login appears to have completed; current URL: ' + page.url());
// small delay to allow app to settle
await page.waitForLoadState('networkidle');
} catch(e) { log('Login did not navigate away within timeout, continuing anyway: '+e); }
} else {
log('No TEST_USER_EMAIL/TEST_USER_PASSWORD provided in env; cannot auto-login.');
}
}
} catch (e) { log('Login detection/attempt failed: '+e); }
// Click the Enter Studio link (first match)
const enterSel = getSel('broadcasts.enter_studio_link');
if (!enterSel) throw new Error('Selector broadcasts.enter_studio_link not found');
try{
log('Waiting for enter studio link: ' + enterSel);
const enterLocator = page.locator(enterSel).first();
await enterLocator.waitFor({ timeout: 10000 });
await enterLocator.click({ force: true });
log('Clicked enter studio link');
} catch(e){ log('Failed clicking enter studio link: '+e); }
// Wait for navigation to prejoin/studio
await page.waitForLoadState('networkidle');
log('After click current URL: ' + page.url());
// If prejoin appears in same page, interact; else try to find new page in context
let studioPage = page;
// Try start camera
const startCameraSel = getSel('prejoin.start_camera_button');
const joinWithoutSel = getSel('prejoin.join_without_devices');
const enterStudioBtnSel = getSel('prejoin.enter_studio_button');
// If start camera exists, try click
if (startCameraSel) {
try{
const loc = studioPage.locator(startCameraSel).first();
await loc.waitFor({ timeout: 6000 });
await loc.click({ force:true });
log('Clicked start camera');
}catch(e){ log('start camera not available or failed: '+e);
// try fallback
if (joinWithoutSel){ try{ const j = studioPage.locator(joinWithoutSel).first(); await j.waitFor({ timeout:2000 }); await j.click({ force:true }); log('Clicked join without devices fallback'); }catch(err){ log('join without devices not found: '+err); } }
}
} else if (joinWithoutSel) {
try{ const j = studioPage.locator(joinWithoutSel).first(); await j.waitFor({ timeout:2000 }); await j.click({ force:true }); log('Clicked join without devices fallback (no startCamera selector)'); }catch(err){ log('join without devices not found (no startCamera selector): '+err); }
}
// Click final Enter if exists
if (enterStudioBtnSel){ try{ const ebtn = studioPage.locator(enterStudioBtnSel).first(); await ebtn.waitFor({ timeout:5000 }); await ebtn.click({ force:true }); log('Clicked final enter studio button'); }catch(e){ log('no final enter button: '+e); } }
// Wait for studio to be ready: look for studio.record_button or studio.add_guest_button
const studioReadySel = getSel('studio.record_button') || getSel('studio.add_guest_button');
if (studioReadySel){
log('Waiting for studio ready selector: ' + studioReadySel);
try{
await studioPage.waitForSelector(studioReadySel, { timeout: STUDIO_TIMEOUT });
log('Studio ready detected on page: ' + studioReadySel + ' url=' + studioPage.url());
}catch(e){
// maybe the app opened in a new tab - check other pages
log('Studio ready selector not found in current page, checking other context pages');
const pages = context.pages();
let found = false;
for (const p of pages){
try{
const u = p.url();
const loc = p.locator(studioReadySel).first();
if (await loc.count() > 0){
log('Found studio ready selector in page: ' + u);
studioPage = p;
found = true;
break;
}
}catch(err){}
}
if (!found) log('Studio ready not found in any page');
}
}
// Capture screenshots
const simShot = '/tmp/playwright_mcp_broadcast.png';
const studioShot = '/tmp/playwright_mcp_studio.png';
try{ await page.screenshot({ path: simShot, fullPage: true }); log('Saved broadcast screenshot: ' + simShot); }catch(e){ log('Failed saving broadcast screenshot: '+e); }
try{ await studioPage.screenshot({ path: studioShot, fullPage: true }); log('Saved studio screenshot: ' + studioShot); }catch(e){ log('Failed saving studio screenshot: '+e); }
log('Flow finished successfully (or attempted)');
}catch(err){ log('Unhandled error in flow: ' + (err && err.stack ? err.stack : String(err))); }
finally{
try{ await browser.close(); log('Browser closed'); }catch(e){ log('Error closing browser: '+e); }
log('Script finished');
}
})();