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'); } })();