#!/usr/bin/env node // run_tests.js — UI test runner using Playwright + Chromium headless shell const { chromium } = require('playwright-core'); const fs = require('fs'); const path = require('path'); const CHROMIUM_PATH = path.join( process.env.HOME, '.cache/ms-playwright/chromium_headless_shell-1223/chrome-headless-shell-linux64/chrome-headless-shell' ); const LD_LIBRARY_PATH = [ path.join(process.env.HOME, 'chromium-libs/libs/usr/lib/x86_64-linux-gnu'), path.join(process.env.HOME, 'chromium-libs/libs/lib/x86_64-linux-gnu') ].join(':'); const { parseTestCases } = require('./parse_testcases.js'); // Parse args const testCasesFile = process.argv[2]; const outputDir = process.argv[3]; if (!testCasesFile || !outputDir) { console.error('Usage: node run_tests.js '); process.exit(1); } function loadTestCases(filePath) { const content = fs.readFileSync(path.resolve(filePath), 'utf8'); return parseTestCases(content); } async function launchBrowser(viewport) { const browser = await chromium.launch({ executablePath: CHROMIUM_PATH, headless: true, args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], env: { ...process.env, LD_LIBRARY_PATH } }); const context = await browser.newContext({ viewport: viewport === 'mobile' ? { width: 375, height: 812 } : { width: 1280, height: 720 }, deviceScaleFactor: viewport === 'mobile' ? 2 : 1, isMobile: viewport === 'mobile', userAgent: viewport === 'mobile' ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' : undefined }); return { browser, context }; } async function executeStep(page, step, screenshotDir, testId, viewportLabel) { const prefix = `${testId}-${viewportLabel}`; switch (step.action) { case 'navigate': await page.goto(step.value, { waitUntil: 'networkidle', timeout: 30000 }); break; case 'wait': await page.waitForTimeout(typeof step.value === 'number' ? step.value : parseInt(step.value)); break; case 'click': try { await page.click(step.value, { timeout: 5000 }); } catch (e) { return { error: `Click failed on "${step.value}": ${e.message.slice(0, 100)}` }; } break; case 'scroll': await page.evaluate((px) => window.scrollBy(0, parseInt(px)), String(step.value)); break; case 'screenshot': { const filename = `${prefix}-${step.value}.png`; await page.screenshot({ path: path.join(screenshotDir, filename), fullPage: false }); return { screenshot: filename }; } case 'check-visual': { const filename = `${prefix}-check-${Date.now()}.png`; await page.screenshot({ path: path.join(screenshotDir, filename), fullPage: false }); return { screenshot: filename, checkDescription: step.value }; } case 'resize': { const [w, h] = step.value.split('x').map(Number); await page.setViewportSize({ width: w, height: h }); break; } default: return { warning: `Unknown action: ${step.action}` }; } return null; } async function runTest(test, viewportLabel, screenshotDir) { const result = { id: test.id, name: test.name, viewport: viewportLabel, status: 'completed', screenshots: [], checks: [], errors: [] }; let browser, context; try { ({ browser, context } = await launchBrowser(viewportLabel)); const page = await context.newPage(); for (const step of test.steps) { const stepResult = await executeStep(page, step, screenshotDir, test.id, viewportLabel); if (stepResult) { if (stepResult.error) { result.errors.push(stepResult.error); // Continue with remaining steps } if (stepResult.screenshot) { result.screenshots.push(stepResult.screenshot); if (stepResult.checkDescription) { result.checks.push({ description: stepResult.checkDescription, screenshot: stepResult.screenshot }); } } } } if (result.errors.length > 0) { result.status = 'completed_with_errors'; } } catch (e) { result.status = 'failed'; result.errors.push(e.message.slice(0, 200)); } finally { if (browser) await browser.close(); } return result; } async function main() { // Load test cases const tests = loadTestCases(testCasesFile); if (tests.length === 0) { console.error('No UI test cases found in', testCasesFile); process.exit(1); } console.log(`Found ${tests.length} UI test(s) in ${path.basename(testCasesFile)}`); // Create output dirs const screenshotDir = path.join(outputDir, 'screenshots'); fs.mkdirSync(screenshotDir, { recursive: true }); const allResults = []; for (const test of tests) { const viewports = test.viewport === 'both' ? ['desktop', 'mobile'] : [test.viewport]; for (const vp of viewports) { console.log(` Running: ${test.id} — ${test.name} [${vp}]`); const result = await runTest(test, vp, screenshotDir); allResults.push(result); const icon = result.status === 'completed' ? '✓' : result.status === 'completed_with_errors' ? '⚠' : '✗'; console.log(` ${icon} ${result.status} — ${result.screenshots.length} screenshot(s)`); } } // Write results.json const report = { timestamp: new Date().toISOString(), testFile: path.basename(testCasesFile), results: allResults }; const resultsPath = path.join(outputDir, 'results.json'); fs.writeFileSync(resultsPath, JSON.stringify(report, null, 2)); console.log(`\nResults saved to: ${resultsPath}`); console.log(`Screenshots: ${screenshotDir}/`); // Summary const completed = allResults.filter(r => r.status === 'completed').length; const withErrors = allResults.filter(r => r.status === 'completed_with_errors').length; const failed = allResults.filter(r => r.status === 'failed').length; console.log(`\nSummary: ${allResults.length} runs | ✓ ${completed} | ⚠ ${withErrors} | ✗ ${failed}`); if (failed > 0) process.exit(1); } main().catch(e => { console.error('Fatal error:', e.message); process.exit(1); });