286 lines
15 KiB
JavaScript
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');
|
|
}
|
|
})();
|