import { spawn } from 'child_process'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); // Path to sitespeed.io — works both locally and in Docker const SITESPEED_BIN = process.env.SITESPEED_BIN || join(__dirname, '..', 'sitespeed.io', 'bin', 'sitespeed.js'); export function runTest(job, onLine) { return new Promise((resolve, reject) => { const outputFolder = join(__dirname, 'reports', job.id); const args = [ SITESPEED_BIN, job.url, '--browser', job.browser, '-n', String(job.runs), '--outputFolder', outputFolder, '--json', '--sustainable.enable', '--axe.enable', '--coach', ]; if (job.mobile) { args.push('--mobile'); } // Chrome needs these flags when running inside Docker if (process.env.IN_DOCKER) { args.push('--browsertime.chrome.args', 'no-sandbox'); args.push('--browsertime.chrome.args', 'disable-dev-shm-usage'); // Disable GPU compositing — not needed in Xvfb and can cause crashes args.push('--browsertime.chrome.args', 'disable-gpu'); } onLine(`[runner] Starting: node ${args.slice(0, 3).join(' ')} ...`); onLine(`[runner] Output folder: ${outputFolder}`); const child = spawn('node', args, { cwd: __dirname, env: { ...process.env, DISPLAY: process.env.DISPLAY || ':99' }, }); child.stdout.on('data', (data) => { const lines = data.toString().split('\n').filter(Boolean); for (const line of lines) onLine(line); }); child.stderr.on('data', (data) => { const lines = data.toString().split('\n').filter(Boolean); for (const line of lines) onLine(`[stderr] ${line}`); }); child.on('close', (code) => { if (code === 0) { resolve(outputFolder); } else { reject(new Error(`sitespeed.io exited with code ${code}`)); } }); child.on('error', (err) => { reject(new Error(`Failed to spawn sitespeed.io: ${err.message}`)); }); }); }