197 lines
6.2 KiB
JavaScript
197 lines
6.2 KiB
JavaScript
#!/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 <test-cases.md> <output-dir>');
|
|
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);
|
|
});
|